@fleetless/sdk 3.0.0 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  "use strict";
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -143,7 +144,7 @@ var HttpClient = class {
143
144
  * RequestOptionsFor<P>` would type-check by construction — the cast
144
145
  * bypasses the very check this exists to add, which is the identical
145
146
  * "special case that quietly exempts calls from the general rule" shape
146
- * this file has already been caught by once. Every current call site to a route
147
+ * this file has already made once. Every current call site to a route
147
148
  * with no body already passes `{}` explicitly for exactly this reason.
148
149
  */
149
150
  async request(path, options) {
@@ -440,7 +441,7 @@ function createAssetsApi(http) {
440
441
  };
441
442
  }
442
443
 
443
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/common.js
444
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/common.js
444
445
  var import_zod = require("zod");
445
446
  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.";
446
447
  var slug = import_zod.z.string().min(2).max(63).regex(/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/, SLUG_RULE);
@@ -464,7 +465,7 @@ var applyError = import_zod.z.object({
464
465
  details: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
465
466
  });
466
467
 
467
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/mcp.js
468
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/mcp.js
468
469
  var import_zod2 = require("zod");
469
470
  function mcpAppEndpointPath(appIdentifier2) {
470
471
  return `/mcp/${appIdentifier2}`;
@@ -513,10 +514,10 @@ var mcpRolePreviewResponse = import_zod2.z.object({
513
514
  });
514
515
  var MCP_ASSET_LINK_TTL_MS = 15 * 60 * 1e3;
515
516
 
516
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/protocol.js
517
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/protocol.js
517
518
  var import_zod8 = require("zod");
518
519
 
519
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/assets.js
520
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/assets.js
520
521
  var import_zod3 = require("zod");
521
522
  var assetKind = import_zod3.z.enum(["urdf", "mesh", "texture", "other"]);
522
523
  var asset = import_zod3.z.object({
@@ -531,7 +532,7 @@ var asset = import_zod3.z.object({
531
532
  * against their own workspace, and matching is the whole job when a sync
532
533
  * comes back incomplete.
533
534
  *
534
- * **The naming rule for a file nothing in the URDF names (W7a, D2).** A
535
+ * **The naming rule for a file nothing in the URDF names.** A
535
536
  * `.dae` carries its own image references — `<init_from>textures/skin.png`
536
537
  * — resolved by the renderer against *the `.dae`'s own directory*, and no
537
538
  * `package://` URI for them appears anywhere in the URDF. The rule is:
@@ -539,12 +540,11 @@ var asset = import_zod3.z.object({
539
540
  * name = the .dae's package:// URI, directory part,
540
541
  * joined with the internal reference, normalized.
541
542
  *
542
- * So `package://rx1_description/meshes/arm.dae` referencing
543
+ * So `package://robot_description/meshes/arm.dae` referencing
543
544
  * `textures/skin.png` uploads as
544
- * `package://rx1_description/meshes/textures/skin.png`.
545
+ * `package://robot_description/meshes/textures/skin.png`.
545
546
  *
546
- * **This is the design's single point of failure and it is stated before
547
- * anything is built against it.** three.js resolves that internal reference
547
+ * **Renderers depend on this rule holding.** three.js resolves that internal reference
548
548
  * relative to wherever it loaded the `.dae` from and asks the loading
549
549
  * manager for the result; the client can only answer if the asset's name
550
550
  * still carries the same **relative tail** (`textures/skin.png`) that the
@@ -555,9 +555,8 @@ var asset = import_zod3.z.object({
555
555
  *
556
556
  * A reference that escapes its package (`../../etc/passwd`) is **not**
557
557
  * renamed into something harmless — it is refused at the producer, by the
558
- * same containment check W7's K2 fix applied to `package://` resolution.
559
- * Two identical rules, one of which is enforced and one of which is
560
- * documented, is how W7's traversal happened in the first place.
558
+ * same containment check that guards `package://` resolution. Two identical
559
+ * rules, one enforced and one only documented, is how a traversal gets in.
561
560
  */
562
561
  name: import_zod3.z.string().min(1).max(500).meta({
563
562
  description: "What the robot called it \u2014 for a mesh, the `package://` URI the URDF references, verbatim, which is the only string a developer can match against their own workspace. A file the URDF never names (an image a `.dae` loads for itself) is named by joining the mesh's own directory with that internal reference."
@@ -591,18 +590,17 @@ var urdfCompleteness = import_zod3.z.object({
591
590
  description: "How many distinct meshes the URDF references."
592
591
  }),
593
592
  /**
594
- * **Was fehlt, und wovon (W9b, DEF-081).**
593
+ * **What is missing, and what kind of thing it was.**
595
594
  *
596
- * Vorher ein blankes `string[]`. Die Console meldete daraufhin *„N meshes
597
- * missing from the workspace"* — auch für eine fehlende **Textur**, während
598
- * `mesh_count` daneben eine andere Zahl nannte: zwei Angaben über denselben
599
- * Gegenstand, die einander widersprechen.
595
+ * Each entry carries its element rather than only its URI. Without that a
596
+ * client can only report every entry as a missing mesh, which contradicts
597
+ * `mesh_count` printed beside it — two statements about the same subject
598
+ * that disagree.
600
599
  *
601
- * Die Cloud wusste es die ganze Zeit: `extractReferencesByElement` markiert
602
- * jede Referenz mit ihrem Element und `buildAssetListResponse` warf die
603
- * Markierung wieder weg. **Die Antwort im Client zu raten wäre genau die
604
- * „neue Kopie", die die Registerzeile ausdrücklich ablehnt** — eine zweite
605
- * Herleitung derselben Tatsache, die von der ersten abweichen kann.
600
+ * The producer knows the element when it extracts the reference, so it
601
+ * travels with it. A client that re-derived it from the file extension
602
+ * would be a second derivation of the same fact, free to diverge from the
603
+ * first.
606
604
  */
607
605
  missing: import_zod3.z.array(import_zod3.z.object({
608
606
  uri: import_zod3.z.string().min(1).max(500).meta({
@@ -650,23 +648,20 @@ var assetFailure = import_zod3.z.object({
650
648
  description: "Why it failed. `unresolvable` means the reference names nothing the producer can find or may read, and is **permanent** \u2014 the only kind reconciliation may treat as gone. `upload_failed` means the bytes exist and the transfer did not succeed, `refused` means it was never attempted because a producer-side ceiling was hit, and `too_large` means it exceeds the upload limit and carries both numbers in `details`."
651
649
  }),
652
650
  /**
653
- * **Die zwei Zahlen, und warum `too_large` eine eigene Art ist (W9b).**
651
+ * **The two numbers, and why `too_large` is a kind of its own.**
654
652
  *
655
- * `refused` bedeutet *„nie versucht, weil eine Decke des Erzeugers erreicht
656
- * wurde"* — das passt auf eine Datei, die wegen ihrer Größe gar nicht erst
657
- * gelesen wurde, **und ebenso auf die Sammel-Sentinel**, mit der ein Sync
658
- * aufhört, einzelne Fehler zu benennen. Beides unter eine Art zu legen wäre
659
- * derselbe Fehler, den W9a eine Welle zuvor ausgeräumt hat: zwei Fakten auf
660
- * einem Schlüssel, von denen jeder den anderen überschreibt.
653
+ * `refused` means *never attempted, because a producer-side ceiling was
654
+ * hit*. That fits a file skipped for its size **and** the single collective
655
+ * entry a sync emits when it stops naming individual failures. Filing both
656
+ * under one kind puts two facts on one key, each overwriting the other.
661
657
  *
662
- * Und ein Grund ohne Zahlen ist kein Grund, mit dem jemand etwas anfangen
663
- * kann. *„Zu groß"* beantwortet nicht, ob das Mesh zu verkleinern ist oder
664
- * die Grenze zu heben — `limit_bytes` und `size_bytes` tun es.
658
+ * A reason without numbers is not one a caller can act on. *"Too large"*
659
+ * does not answer whether to shrink the mesh or raise the limit;
660
+ * `limit_bytes` and `size_bytes` do.
665
661
  *
666
- * Abwesend für jede andere Art — ein erzwungenes `details: null` auf jedem
667
- * `unresolvable` kauft nichts. Die Paarung ist unten **erzwungen**, nicht
668
- * beschrieben: ein Feld, dessen Regel nur im Kommentar steht, ist eine
669
- * Bitte.
662
+ * Absent for every other kind — a forced `details: null` on every
663
+ * `unresolvable` buys nothing. The pairing is **enforced** below, not merely
664
+ * described: a field whose rule lives only in a comment is a request.
670
665
  */
671
666
  details: assetTooLargeDetails.nullish().meta({
672
667
  description: "The two numbers behind a `too_large` failure, and absent for every other kind \u2014 a forced `null` on every `unresolvable` entry buys nothing. The pairing is enforced, not merely described."
@@ -693,48 +688,28 @@ var assetSyncStatus = import_zod3.z.object({
693
688
  description: "How many files this sync set out to transfer. It is `0` until the producer has finished working out what there is."
694
689
  }),
695
690
  /**
696
- * **Every entry says *why*, because reconciliation could not work without
697
- * it and a developer could not read it without it** (W7a review, André's
698
- * decision to fix rather than defer).
691
+ * **Every entry says *why*.**
699
692
  *
700
- * It was a flat `string[]`, and **six producers wrote three different facts
701
- * into it indistinguishably**: a reference that resolves to nothing in the
702
- * workspace, a file that exists and whose transfer failed, and — since R9's
703
- * ceiling — one that was never attempted at all. The cost was paid twice
704
- * over. Reconciliation cannot tell *"no longer referenced"* from
705
- * *"referenced and not delivered"*, so N14 had to decline reconciling **any**
706
- * partial sync, leaving legitimately-removed assets stored and charged until
707
- * the next clean one. And the console prints the whole array under *"these
708
- * meshes could not be resolved"*, so a URDF upload failure — which arrives
709
- * as the literal `robot_description` — is shown to a developer as a mesh
710
- * they should go and find.
693
+ * A flat `string[]` cannot carry three different facts distinguishably: a
694
+ * reference that resolves to nothing in the workspace, a file that exists
695
+ * and whose transfer failed, and one that was never attempted at all.
711
696
  *
712
- * `unresolvable` is the only kind reconciliation may drop: it is the only
713
- * one that means *this will not come back*. `upload_failed` and `refused`
714
- * both mean *we meant to provide this and did not*, which is the distinction
715
- * the union needs and the field could not carry.
697
+ * The distinction is what makes reconciliation possible. `unresolvable` is
698
+ * the only kind that means *this will not come back*, so it is the only kind
699
+ * an unreferenced-asset sweep may act on. `upload_failed` and `refused` both
700
+ * mean *this was meant to be provided and was not*, and dropping the asset
701
+ * on either would delete something still wanted.
716
702
  *
717
- * **Bounded, and the bound is a rule this file already wrote down one field
718
- * over** (Kassandra-W7a, W7a review). `asset.name` is `max(500)`; the same
719
- * names travelling here had no per-entry cap and no array cap at all.
703
+ * **Bounded, per entry and in total.** One mesh reference can expand into a
704
+ * list bounded only by the text of the `.dae` it points at, and the whole
705
+ * status travels in one WebSocket frame with a maximum payload. Without a
706
+ * cap the outcome is not a dropped frame but the robot's socket closed
707
+ * mid-sync by a file in its own workspace. At most 1000 entries of at most
708
+ * 500 bytes is about 0.5 MiB of names, comfortably inside that frame.
720
709
  *
721
- * Why it matters became reachable in W7a. Before R6 an unresolvable
722
- * reference was silently dropped, so this array could only grow with files
723
- * that existed and failed to upload — bounded by the workspace. Once a
724
- * `.dae`'s internal references are reported, **one mesh reference expands
725
- * into a list bounded only by that file's own text.** Measured: a
726
- * 2,120,745-byte `.dae` with 17,331 unresolvable `<init_from>` refs produces
727
- * a terminal frame of 2,097,184 bytes — **32 bytes over
728
- * `MAX_WS_PAYLOAD_BYTES`** — and `ws` enforces `maxPayload` before the frame
729
- * is delivered, so the outcome is not a dropped frame but **the robot's
730
- * socket closed, mid-sync, by a file in its own workspace.**
731
- *
732
- * A producer that hits its own ceiling reports **one** entry saying so
733
- * rather than growing the list — the discipline gate step 5 already demands
734
- * of the upload rate limit: *fail naming the limit, rather than silently
735
- * reporting resolvable meshes as missing.*
736
- *
737
- * 1000 x 500 bytes is ~0.5 MiB of names, comfortably inside a 2 MiB frame.
710
+ * A producer that reaches its own ceiling reports **one** entry saying so
711
+ * rather than growing the list: fail naming the limit, rather than silently
712
+ * reporting resolvable meshes as missing.
738
713
  */
739
714
  failed: import_zod3.z.array(assetFailure).max(1e3).meta({
740
715
  description: "What could not be provided, one entry per reference, each saying why. Required rather than optional: a sync that quietly drops three meshes and reports success moves the failure into somebody's renderer, where it shows up as a robot with missing limbs and no cause. At most `1000` entries \u2014 a producer at its own ceiling reports one entry saying so rather than growing the list."
@@ -754,14 +729,13 @@ var assetListResponse = import_zod3.z.object({
754
729
  description: "Every asset stored for this robot: the URDF, the meshes it references, and the textures those paint with."
755
730
  }),
756
731
  /**
757
- * Der gerade laufende Sync, oder `null` (W9b, DEF-147).
732
+ * The sync running right now, or `null`.
758
733
  *
759
- * **Der Fall, für den das hier steht, ist der Neuladen-Fall.** Die Console
760
- * hielt die `sync_id` nur im Speicher; ein Reload verlor die Fortschritts-
761
- * anzeige, und der Zustand war serverseitig da, über
762
- * `GET .../assets/sync/<id>` abfragbar — nur erreichte ihn niemand mehr, der
763
- * die id nicht aufgehoben hatte. Eine Seite, die frisch lädt, drückt keinen
764
- * Knopf; sie fragt diese Liste. Also muss die Liste es sagen.
734
+ * **This field exists for the reload case.** A client that holds the
735
+ * `sync_id` only in memory loses its progress display on a refresh, and the
736
+ * state is still there server-side under `GET .../assets/sync/<id>` —
737
+ * unreachable to anyone who did not keep the id. A page that loads fresh
738
+ * presses no button; it asks this list, so this list has to say.
765
739
  */
766
740
  active_sync: assetSyncStatus.nullable().meta({
767
741
  description: "The sync running right now, or `null`. It is on this list so a page that reloads and has lost the sync id can still show progress \u2014 a freshly loaded page presses no button, it asks this list."
@@ -771,30 +745,20 @@ var assetListResponse = import_zod3.z.object({
771
745
  }),
772
746
  /**
773
747
  * What the connected bridge says it *could* transfer, which is deliberately
774
- * separate from what has been transferred (§4.6: the bridge "meldet nur
775
- * Verfügbarkeit"). `null` when no bridge is connected — distinct from
776
- * `false`, because "no robot is online to ask" and "the robot has no URDF"
777
- * send a developer to two different places.
778
- *
779
- * **All three states are reachable as of W7a (R7).** They were not: the bridge
780
- * used to report availability from a subscription callback, which fires only
781
- * when a publisher *sends* something, so it could notice presence and never
782
- * absence — a robot that lost its URDF left the cloud holding the last thing
783
- * it heard, forever, and `true` was sticky. The fix is an **active**
784
- * `count_publishers` query on the bridge's own timer.
748
+ * separate from what has been transferred. `null` when no bridge is
749
+ * connected — distinct from `false`, because "no robot is online to ask" and
750
+ * "the robot has no URDF" send a developer to two different places.
785
751
  *
786
- * **What a consumer still needs to know is the clock, not the gap.** An
787
- * ungraceful loss — the publisher process killed rather than shut down — is
788
- * noticed on **DDS's liveliness timeout**, not on the bridge's check
789
- * interval. Measured against a real bridge: ~1.6 s when the publisher calls
790
- * `destroy_node()`, **~19 s when it is `SIGKILL`ed**. So `true` can outlive
791
- * the truth by some seconds after a crash, and no amount of polling on our
792
- * side shortens it.
752
+ * The bridge answers by actively counting publishers on its own timer, so
753
+ * both the appearance and the disappearance of a robot description are
754
+ * noticed. A callback-driven answer can only see presence.
793
755
  *
794
- * The sticky-`true` gap was found by Rosie-W7 checking her own work against
795
- * the camera-health row of identical shape; the DDS clock was measured by
796
- * Rosie-W7a closing it, and this comment was still describing the gap a wave
797
- * after it was fixed (Momus-W7a, W7a review).
756
+ * **What a consumer needs to know is the clock.** An ungraceful loss — the
757
+ * publisher process killed rather than shut down — is noticed on DDS's
758
+ * liveliness timeout, not on the bridge's check interval. A clean
759
+ * `destroy_node()` is visible in a second or two; a killed process can take
760
+ * around twenty. So `true` can outlive the truth by some seconds after a
761
+ * crash, and no amount of polling shortens it.
798
762
  */
799
763
  urdf_available: import_zod3.z.boolean().nullable().meta({
800
764
  description: 'What the connected bridge says it *could* transfer, which is deliberately separate from what has been transferred. `null` when no bridge is connected \u2014 distinct from `false`, because "no robot is online to ask" and "the robot has no URDF" send a developer to two different places. After a publisher is killed rather than shut down this can read `true` for some seconds, on the underlying DDS liveliness timeout rather than on any check made here.'
@@ -807,14 +771,14 @@ var missingAssetQuery = import_zod3.z.object({
807
771
  }).meta({ description: "The one optional parameter of the missing-asset placeholder; it names the reference in the refusal." });
808
772
  var assetSyncBusyDetails = import_zod3.z.object({
809
773
  sync_id: import_zod3.z.uuid(),
810
- /** Wann er begann — damit „läuft noch" von „hängt seit einer Stunde" unterscheidbar ist. */
774
+ /** When it started, so "still running" is distinguishable from "stuck for an hour". */
811
775
  started_at_ms: import_zod3.z.number().int().nonnegative()
812
776
  });
813
777
 
814
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/config.js
778
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/config.js
815
779
  var import_zod5 = require("zod");
816
780
 
817
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/alerts.js
781
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/alerts.js
818
782
  var import_zod4 = require("zod");
819
783
  var alertRowCondition = import_zod4.z.discriminatedUnion("kind", [
820
784
  import_zod4.z.strictObject({
@@ -844,7 +808,7 @@ var datapointAlertRow = import_zod4.z.object({
844
808
  severity: alertSeverity,
845
809
  condition: alertRowCondition,
846
810
  state: alertState,
847
- /** `null` only until the first evaluation writes a state; every alert is created `ok` (D2), so in practice this is set from creation onward. */
811
+ /** `null` only until the first evaluation writes a state; every alert is created `ok`, so in practice this is set from creation onward. */
848
812
  state_since: import_zod4.z.iso.datetime().nullable(),
849
813
  /**
850
814
  * The value at the alert's last state transition — written only when the
@@ -884,7 +848,7 @@ var putDatapointDisplayRequest = import_zod4.z.object({
884
848
  y_max: import_zod4.z.number().finite().nullable()
885
849
  }).strict();
886
850
 
887
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/config.js
851
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/config.js
888
852
  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:`.";
889
853
  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:`.";
890
854
  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.";
@@ -1124,10 +1088,9 @@ var datapointAlert = strictObject({
1124
1088
  * `getInsertTextForProperty` (`yaml.worker.js:8520`) takes
1125
1089
  * `defaultSnippets[0].body` only when a node carries **exactly one**
1126
1090
  * snippet, so accepting `condition` from the key list writes the bare key
1127
- * here where every other node this wave touched writes its whole block.
1128
- * The two stay anyway: the value position — a developer who has written
1129
- * `condition:` and pressed ⏎ — is where the question "what goes here?" is
1130
- * actually asked, and that is the position this wave exists to answer.
1091
+ * here where every other node writes its whole block. The two stay anyway:
1092
+ * the value position — a developer who has written `condition:` and pressed
1093
+ * ⏎ — is where the question "what goes here?" is actually asked.
1131
1094
  * Merging them into one would buy back the key completion by deleting the
1132
1095
  * choice the schema deliberately does not name, which is the worse trade;
1133
1096
  * anyone tempted to make it should change the key-completion behaviour
@@ -1246,9 +1209,9 @@ var NUMERIC_DATAPOINT_SNIPPET = {
1246
1209
  /**
1247
1210
  * The quotes inside `unit` are part of the inserted text and are not
1248
1211
  * decoration. A body string is written into the document verbatim, and `%`
1249
- * is a YAML directive indicator: measured with `yaml` 2.9.0, `unit: %` is a
1250
- * **syntax error** ("Plain value cannot start with directive indicator
1251
- * character %") while `unit: "%"` parses to `%`. Nothing between here and
1212
+ * is a YAML directive indicator: `unit: %` is a **syntax error** ("Plain
1213
+ * value cannot start with directive indicator character %") while
1214
+ * `unit: "%"` parses to `%`. Nothing between here and
1252
1215
  * the buffer quotes a scalar for us.
1253
1216
  */
1254
1217
  numeric: { scale: 100, unit: '"%"', decimals: 1 },
@@ -1694,15 +1657,14 @@ var cameraSource = import_zod5.z.discriminatedUnion("kind", [
1694
1657
  * `rosTypeName` accepts either, publish accepts either, no diagnostic
1695
1658
  * fires anywhere, and the bridge then subscribes with the wrong type and
1696
1659
  * delivers no frames. A snippet supplying a wrong answer where it could
1697
- * have supplied a question is this project's *check that cannot fire*,
1698
- * arriving through a hint the developer trusts.
1660
+ * have supplied a question is a defect arriving through a hint the
1661
+ * developer trusts.
1699
1662
  *
1700
1663
  * `kind: 'ros'` stays a literal, because the branch really does fix it.
1701
1664
  *
1702
- * Measured through the actual pipeline rather than assumed, because
1703
- * choice syntax is the one construct here that three layers must each
1665
+ * Choice syntax is the one construct here that three layers must each
1704
1666
  * pass through unharmed: yaml-language-server's `stringifyObject` emits
1705
- * the body verbatim, and monaco-editor 0.52.2's `SnippetParser` parses
1667
+ * the body verbatim, and monaco-editor's `SnippetParser` parses
1706
1668
  * `${2|a,b|}` into a placeholder carrying both options whose
1707
1669
  * `toString()` — the text on the buffer before anyone chooses — is the
1708
1670
  * first one. So a developer who tabs past this gets a document
@@ -1721,16 +1683,12 @@ var cameraSource = import_zod5.z.discriminatedUnion("kind", [
1721
1683
  description: "Selects the RTSP source: this camera then carries `url`, and optionally `transport` and `credentials`."
1722
1684
  }),
1723
1685
  /**
1724
- * Scheme-constrained deliberately. The playbook drafted `z.string().url()`
1725
- * here and the shipped contract was `z.string().min(1).max(2048)` — nobody
1726
- * recorded the change, and the W6 review found the consequence: the bridge
1727
- * opens these with libraries that honour `file:` and `ftp:`, so an
1728
- * unconstrained URL turns a configuration document into an arbitrary
1729
- * local-file read on the robot, with the two distinct failure codes
1730
- * doubling as a file-existence oracle. Spec §7.6 is ROS-pure exposure with
1731
- * no shell or http features; that rule came back by omission rather than
1732
- * by intent. The bridge re-checks this too — a robot must not become a
1733
- * file server because a validator changed.
1686
+ * Scheme-constrained deliberately. The bridge opens these with libraries
1687
+ * that honour `file:` and `ftp:`, so an unconstrained URL turns a
1688
+ * configuration document into an arbitrary local-file read on the robot,
1689
+ * with the two distinct failure codes doubling as a file-existence oracle.
1690
+ * The bridge re-checks this too — a robot must not become a file server
1691
+ * because a validator changed.
1734
1692
  */
1735
1693
  url: import_zod5.z.string().min(1).max(2048).regex(/^rtsps?:\/\//i, RTSP_URL_RULE).meta({
1736
1694
  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.",
@@ -1768,8 +1726,8 @@ var cameraSource = import_zod5.z.discriminatedUnion("kind", [
1768
1726
  /**
1769
1727
  * The host and the path this branch's own snippet body inserts, and the
1770
1728
  * URL its rule sentence names — one answer to "what goes here?", not a
1771
- * third. The sibling `rtsp` url had an `examples` from the first day and
1772
- * this position was the format's only silent URL (§1.1).
1729
+ * third. Every URL position in the format carries an example; a silent
1730
+ * one is the position a developer has to guess at.
1773
1731
  */
1774
1732
  examples: ["http://cam-1.plant.local/video.mjpg"]
1775
1733
  }),
@@ -1794,16 +1752,14 @@ var cameraSource = import_zod5.z.discriminatedUnion("kind", [
1794
1752
  * on the robot, never by the cloud.
1795
1753
  *
1796
1754
  * Constrained to `/dev/` for the same reason the `rtsp` and `mjpeg` URLs
1797
- * are constrained to their schemes, and it was missed the first time
1798
- * (Momus, W6 verification). The device string reaches
1755
+ * are constrained to their schemes. The device string reaches
1799
1756
  * `cv2.VideoCapture(device)` on the robot, and OpenCV does not restrict
1800
- * itself to devices: measured on cv2 4.5.4, an ordinary local video file
1801
- * opens and its pixels are published to the cloud, and so does
1802
- * `http://127.0.0.1:8899/secret.jpg`. Unconstrained, this field is an
1803
- * arbitrary local-file read *and* an outbound fetch from inside the robot
1804
- * — the §7.6 violation closed for the other two source kinds, reachable
1805
- * through the fourth, because "it is just a device path" read like a
1806
- * reason not to check.
1757
+ * itself to devices: an ordinary local video file opens and its pixels are
1758
+ * published to the cloud, and so does an `http://` URL pointing back inside
1759
+ * the robot's own network. Unconstrained, this field is an arbitrary
1760
+ * local-file read *and* an outbound fetch from inside the robot — the same
1761
+ * hole closed for the other two source kinds, reachable through the fourth,
1762
+ * because "it is just a device path" reads like a reason not to check.
1807
1763
  *
1808
1764
  * Narrower than the URL hole in one respect worth recording: a non-media
1809
1765
  * file and a missing file both fail to open, so this branch never worked
@@ -1939,7 +1895,7 @@ var configState = import_zod5.z.object({
1939
1895
  applied_errors: import_zod5.z.array(applyError).nullable()
1940
1896
  });
1941
1897
 
1942
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/introspection.js
1898
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/introspection.js
1943
1899
  var import_zod6 = require("zod");
1944
1900
  var rosGraphEntry = import_zod6.z.object({
1945
1901
  name: rosName,
@@ -1978,7 +1934,7 @@ var typeDefinition = import_zod6.z.discriminatedUnion("kind", [
1978
1934
  })
1979
1935
  ]);
1980
1936
 
1981
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/jobs.js
1937
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/jobs.js
1982
1938
  var import_zod7 = require("zod");
1983
1939
  var jobState = import_zod7.z.enum(["running", "succeeded", "failed", "cancelled", "lost"]);
1984
1940
  var job = import_zod7.z.object({
@@ -1999,17 +1955,17 @@ var job = import_zod7.z.object({
1999
1955
  description: "When this job last changed, as an ISO 8601 timestamp."
2000
1956
  }),
2001
1957
  /**
2002
- * A monotonic counter, ascending in mint order (W7), and the **named**
2003
- * tiebreaker for any listing that claims an order.
1958
+ * A monotonic counter, ascending in mint order, and the **named** tiebreaker
1959
+ * for any listing that claims an order.
2004
1960
  *
2005
1961
  * `started_at` is not a total order: two jobs minted in the same millisecond
2006
1962
  * sort against each other arbitrarily, and arbitrarily means *differently on
2007
1963
  * each query* — so `GET /api/robots/:id/jobs`, which documents "newest
2008
- * first", can show one twice and the other not at all. Exactly the defect
2009
- * `auditEvent.seq` was added for in W6b, in a route the same wave shipped.
1964
+ * first", can show one twice and the other not at all. `auditEvent.seq`
1965
+ * exists for the same reason on the audit log.
2010
1966
  *
2011
1967
  * **Scoped honestly: per cloud process, per run.** Job state lives in memory
2012
- * (§6.1 — that is why `lost` exists at all), so this counter restarts when
1968
+ * — that is why `lost` exists at all — so this counter restarts when
2013
1969
  * the cloud does, alongside the jobs it orders. Sound, because it only ever
2014
1970
  * orders jobs that coexist in one registry — and stated, because a reader
2015
1971
  * who assumed `auditEvent.seq`'s durable semantics would be wrong.
@@ -2024,14 +1980,10 @@ var job = import_zod7.z.object({
2024
1980
  * Present on `failed`; a human message, plus a code where one exists.
2025
1981
  *
2026
1982
  * `details` exists because a refusal that carries only prose forces every
2027
- * consumer to parse it. W6b shipped `job_queue_full` with a documented
2028
- * `{limit, queued}` payload and **nowhere to put it**: the bridge reports a
2029
- * full queue as a job error, this shape had no `details`, and so the numbers
2030
- * were formatted into the message and lost. The console then rendered a
2031
- * "wait for one of N to finish" alert from a shape nothing in the system
2032
- * produced, and its test built that shape by hand — three repos agreeing
2033
- * with each other about a payload none of them exchanged (Momus, W6b
2034
- * review).
1983
+ * consumer to parse it. A documented payload with nowhere to put it — the
1984
+ * bridge reports a full queue as a job error — ends up formatted into the
1985
+ * message and lost, and every consumer then builds the structured shape by
1986
+ * hand from its own assumption.
2035
1987
  *
2036
1988
  * Optional, because most job errors have nothing structured to add. Where a
2037
1989
  * code has a documented payload — `job_queue_full` has
@@ -2200,7 +2152,7 @@ var jobRunSummary = import_zod7.z.object({
2200
2152
  since_ms: import_zod7.z.number().int().nonnegative()
2201
2153
  });
2202
2154
 
2203
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/protocol.js
2155
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/protocol.js
2204
2156
  var MAX_PATIENCE_MS = 12e4;
2205
2157
  var MIN_PATIENCE_MS = 1e3;
2206
2158
  var activeJob = import_zod8.z.object({
@@ -2214,7 +2166,7 @@ var bridgeHello = import_zod8.z.object({
2214
2166
  token: import_zod8.z.string().min(1),
2215
2167
  bridge_version: import_zod8.z.string().min(1),
2216
2168
  /**
2217
- * Every job this bridge still knows about, right now (spec §6.1, W4).
2169
+ * Every job this bridge still knows about, right now.
2218
2170
  *
2219
2171
  * A reconnect and a restart look **identical** on the wire otherwise: same
2220
2172
  * token, same version, same frame. But they must end differently — after a
@@ -2229,16 +2181,9 @@ var bridgeHello = import_zod8.z.object({
2229
2181
  * has none — which is exactly the truth the cloud needs. A breadcrumb file
2230
2182
  * would only add a window in which the crash beat the write.
2231
2183
  *
2232
- * Defaulted so pre-W4 bridges still parse; they had no jobs, so the empty
2233
- * list is also the correct answer for them.
2234
- *
2235
- * **Renamed from `active_job_ids` in W6b**, when the entries stopped being
2236
- * ids. A field called `_ids` holding objects is the shape this project has
2237
- * repeatedly been caught by — a name that describes what the field used to
2238
- * carry, kept because renaming looked like churn. Nothing is deployed yet
2239
- * (W8 is the first deployment), so the old name is gone rather than
2240
- * accepted alongside the new one: two accepted spellings would have to be
2241
- * supported and reconciled forever, and nobody is asking for that.
2184
+ * Defaulted, so a bridge that sends no such field still parses; a bridge
2185
+ * with no jobs and a bridge that does not report them both mean the cloud
2186
+ * has nothing to keep alive.
2242
2187
  */
2243
2188
  active_jobs: import_zod8.z.array(activeJob).max(500).default([])
2244
2189
  });
@@ -2281,21 +2226,20 @@ var cloudInvoke = import_zod8.z.object({
2281
2226
  job_id: import_zod8.z.uuid(),
2282
2227
  slug,
2283
2228
  /**
2284
- * Already validated against §4.4 rules; the bridge validates structurally.
2229
+ * Already validated against the configuration's parameter rules; the bridge
2230
+ * validates structurally.
2285
2231
  *
2286
2232
  * **Flat, keyed by parameter name** — `{"target_x": 1}`. The key is a key of
2287
- * the entry's `parameters` mapping, not a path into the message. Those were
2288
- * the same thing until FL-002 and are now deliberately decoupled: a
2289
- * parameter keeps its name when the field it fills moves in the message
2290
- * tree, which is the same reason a slug is not a topic name.
2233
+ * the entry's `parameters` mapping, not a path into the message. The two are
2234
+ * deliberately decoupled: a parameter keeps its name when the field it fills
2235
+ * moves in the message tree, which is the same reason a slug is not a topic
2236
+ * name.
2291
2237
  *
2292
- * Three things follow, and the last one got stronger rather than weaker:
2293
- * the key a caller sends is the key a rule names, so a `parameter_invalid`
2294
- * reports something the caller can find; the console binds one input per
2295
- * parameter; and a position the template does not mark with `${…}` cannot
2296
- * be set by any caller at all. That last one used to be a rule about what
2297
- * no `parameterSpec` declared. It is now structural — the value has nowhere
2298
- * to go.
2238
+ * Three things follow: the key a caller sends is the key a rule names, so a
2239
+ * `parameter_invalid` reports something the caller can find; a UI binds one
2240
+ * input per parameter; and a position the template does not mark with
2241
+ * `${…}` cannot be set by any caller at all, structurally — the value has
2242
+ * nowhere to go.
2299
2243
  *
2300
2244
  * The bridge substitutes these values into the entry's `message` template
2301
2245
  * at its placeholder positions. It no longer unflattens a dotted path;
@@ -2303,18 +2247,17 @@ var cloudInvoke = import_zod8.z.object({
2303
2247
  */
2304
2248
  params: import_zod8.z.record(import_zod8.z.string(), import_zod8.z.unknown()),
2305
2249
  /**
2306
- * How long this one call is worth waiting for (W6b), already resolved by
2307
- * the cloud — the caller's `invokeRequest.patience_ms`, or
2250
+ * How long this one call is worth waiting for, already resolved by the
2251
+ * cloud — the caller's `invokeRequest.patience_ms`, or
2308
2252
  * `DEFAULT_PATIENCE_MS` when they named none.
2309
2253
  *
2310
2254
  * **Required here, optional at REST**, deliberately. At the REST edge an
2311
2255
  * absent value is a caller who did not care and gets the default. By the
2312
2256
  * time the frame is on this socket somebody has decided, and the bridge
2313
2257
  * must never be in the position of picking a number the cloud is already
2314
- * counting against — which is what two independent 15 s constants meant in
2315
- * practice: a bridge that gave up at 15.0 s and a cloud that gave up at
2316
- * 15.0 s, agreeing only by accident, with no way to tell whose deadline a
2317
- * caller had actually hit.
2258
+ * counting against. Two independent constants that happen to match give up
2259
+ * at the same moment by accident, with no way to tell whose deadline a
2260
+ * caller actually hit.
2318
2261
  */
2319
2262
  patience_ms: import_zod8.z.number().int().min(MIN_PATIENCE_MS).max(MAX_PATIENCE_MS)
2320
2263
  });
@@ -2462,12 +2405,12 @@ var cloudCameraStart = import_zod8.z.object({
2462
2405
  room: import_zod8.z.string().min(1),
2463
2406
  token: import_zod8.z.string().min(1),
2464
2407
  /**
2465
- * Names **this attempt** (W6b), and is echoed in the `camera_state` that
2466
- * answers it.
2408
+ * Names **this attempt**, and is echoed in the `camera_state` that answers
2409
+ * it.
2467
2410
  *
2468
- * W6a gave `camera_state` a `cause` and said in the same comment that a
2469
- * cause is not a correlation. This is the other half. Start a camera, have
2470
- * it fail slowly, start it again: the first attempt's failure arrives while
2411
+ * `camera_state.cause` says what kind of event a frame is; a cause is not a
2412
+ * correlation, and this is the other half. Start a camera, have it fail
2413
+ * slowly, start it again: the first attempt's failure arrives while
2471
2414
  * the second is in flight, matches on slug, and resolves the attempt it
2472
2415
  * knows nothing about. The viewer is then told the running stream failed,
2473
2416
  * for a reason belonging to an attempt that is already over.
@@ -2501,12 +2444,13 @@ var bridgeAssetProgress = import_zod8.z.object({
2501
2444
  total: import_zod8.z.number().int().nonnegative(),
2502
2445
  /**
2503
2446
  * **Each entry says why** — see `assetFailure` in `assets.ts` for the three
2504
- * kinds and why one word was not enough. The bound is `assets.ts`'s too: a
2505
- * `.dae` with 17,331 unresolvable internal references produced a frame 32
2506
- * bytes over `MAX_WS_PAYLOAD_BYTES`, and `ws` enforces that **before**
2507
- * delivery — so the outcome was the robot's own socket closed, mid-sync, by
2508
- * a file in its workspace (Kassandra-W7a). A producer at its own ceiling
2509
- * reports **one** `refused` entry naming the file, not one per reference.
2447
+ * kinds and why one word is not enough. The bound is `assets.ts`'s too: a
2448
+ * single `.dae` can carry tens of thousands of unresolvable internal
2449
+ * references, which is enough to push this frame past
2450
+ * `MAX_WS_PAYLOAD_BYTES`. That limit is enforced **before** delivery, so the
2451
+ * outcome is not a dropped frame but the robot's own socket closed mid-sync
2452
+ * by a file in its workspace. A producer at its own ceiling reports **one**
2453
+ * `refused` entry naming the file, not one per reference.
2510
2454
  */
2511
2455
  failed: import_zod8.z.array(assetFailure).max(1e3),
2512
2456
  /**
@@ -2518,17 +2462,12 @@ var bridgeAssetProgress = import_zod8.z.object({
2518
2462
  * answers with silence is a backstop nobody can debug, and the alternative
2519
2463
  * on the table was to report every requested URI in `failed`. That would
2520
2464
  * have made `failed` mean two different things at once — *could not be
2521
- * resolved* and *was never attempted* — which is the one-field-two-facts
2522
- * defect this project has now split five times (`set`/`readable`,
2523
- * `truncated`/`truncated_by`, `value`/`sample_count`, `publishing`/`cause`,
2524
- * and camera health's own).
2465
+ * resolved* and *was never attempted* — one field carrying two facts, each
2466
+ * overwriting the other.
2525
2467
  *
2526
2468
  * So: `running` while work is happening, `finished` when the bridge will
2527
2469
  * send no more for this sync, `refused_busy` when it never started because
2528
2470
  * another sync was in flight. `failed` keeps its single meaning.
2529
- *
2530
- * Raised by Rosie-W7, who found the gap by asking what a second request
2531
- * should do rather than picking the silent option.
2532
2471
  */
2533
2472
  state: import_zod8.z.enum(["running", "finished", "refused_busy"])
2534
2473
  });
@@ -2544,15 +2483,13 @@ var bridgeCameraState = import_zod8.z.object({
2544
2483
  publishing: import_zod8.z.boolean(),
2545
2484
  error: import_zod8.z.object({ code: import_zod8.z.string().min(1), message: import_zod8.z.string().min(1) }).nullable(),
2546
2485
  /**
2547
- * Why this frame was sent (W6a).
2486
+ * Why this frame was sent.
2548
2487
  *
2549
2488
  * Without it, `{publishing: false, error: null}` is sent for **three
2550
2489
  * different things** — an answer to `camera_stop`, a stream stopped by a
2551
- * configuration change, and a source that recovered — and the cloud can
2552
- * only tell them apart by remembering what it saw before. Deriving a cause
2553
- * from remembered state is precisely the inference this project keeps
2554
- * finding to be wrong, and W6a exists because four failures had been
2555
- * sharing one silence.
2490
+ * configuration change, and a source that recovered — and the cloud can only
2491
+ * tell them apart by remembering what it saw before. A cause derived from
2492
+ * remembered state is a guess.
2556
2493
  *
2557
2494
  * `'command'` this frame answers a `camera_start` / `camera_stop`.
2558
2495
  * `'source'` unsolicited: the source's own health changed, whether or
@@ -2562,29 +2499,24 @@ var bridgeCameraState = import_zod8.z.object({
2562
2499
  * failure, and it must not be logged as one.
2563
2500
  * `'live_lost'` publishing ended unexpectedly after it had started.
2564
2501
  *
2565
- * Note it does **not** answer "which attempt is this?" — `camera_state`
2566
- * still has no request id, and that remains a named deferral in cluster C.
2567
- * `cause` says what kind of event this is; correlation is a separate fact
2568
- * and giving one field both jobs would be the same mistake again.
2502
+ * It does **not** answer "which attempt is this?" — `request_id` beside it
2503
+ * does. `cause` says what kind of event this is; correlation is a separate
2504
+ * fact, and giving one field both jobs would be the same mistake again.
2569
2505
  *
2570
- * Required, not optional: an absent cause would default to the reading
2571
- * somebody happens to assume, and every frame's sender knows its own
2572
- * reason. Old bridges fail validation on this frame — acceptable while
2573
- * nothing is deployed, and W8 is the first deployment.
2506
+ * Required, not optional: an absent cause would default to whatever reading
2507
+ * the receiver happens to assume, and every frame's sender knows its own
2508
+ * reason.
2574
2509
  */
2575
2510
  cause: import_zod8.z.enum(["command", "source", "config_change", "live_lost"]),
2576
2511
  /**
2577
2512
  * When the **robot** observed this state — bridge capture time, never
2578
- * receive time, the same discipline `timestamp_ms` follows for samples
2579
- * (spec §6.3).
2513
+ * receive time, the same discipline `timestamp_ms` follows for samples.
2580
2514
  *
2581
- * It exists because the cloud stamped `resourceHealthState.changed_at_ms`
2582
- * with its own `Date.now()`, and a **restatement** is by definition an old
2583
- * state re-sent into an empty map. So after a cloud restart every failure —
2584
- * including one from yesterday — was dated to the restart, in the one
2585
- * scenario `changed_at_ms`'s own doc comment was written for: *"a page that
2586
- * loads late must be able to tell a failure from a minute ago from one from
2587
- * yesterday"*.
2515
+ * It exists because a cloud that stamps `resourceHealthState.changed_at_ms`
2516
+ * with its own clock dates every **restatement** to the moment it restarted
2517
+ * — a restatement is by definition an old state re-sent into an empty map.
2518
+ * That destroys exactly what `changed_at_ms` is for: a page that loads late
2519
+ * must be able to tell a failure from a minute ago from one from yesterday.
2588
2520
  *
2589
2521
  * On a restatement this carries **when the state was first observed**, not
2590
2522
  * when the frame was sent. A bridge that re-states a failure it has held for
@@ -2592,7 +2524,7 @@ var bridgeCameraState = import_zod8.z.object({
2592
2524
  */
2593
2525
  observed_at_ms: import_zod8.z.number().int().nonnegative(),
2594
2526
  /**
2595
- * Which request this frame answers (W6b), or `null` when it answers none.
2527
+ * Which request this frame answers, or `null` when it answers none.
2596
2528
  *
2597
2529
  * `null` is not a gap and must not be treated as one: a `cause: 'source'`
2598
2530
  * frame — the unsolicited health report that makes a wrong password visible
@@ -2609,21 +2541,21 @@ var bridgeCameraState = import_zod8.z.object({
2609
2541
  * **The pairing rule is not in this schema, deliberately.** "Non-null iff
2610
2542
  * `cause === 'command'`" is a cross-field constraint; a zod `.refine()`
2611
2543
  * would express it at runtime and then **disappear** from the generated
2612
- * JSON Schema, which is what the bridge vendors. The cloud would reject
2613
- * frames the bridge had validated as correct — the same artifact/runtime
2614
- * divergence that `.default()` publishing as `required` has produced four
2615
- * times in this project, only pointing the other way. The rule is enforced
2544
+ * JSON Schema, which is what a non-TypeScript bridge validates against. The
2545
+ * cloud would reject frames the bridge had validated as correct — the same
2546
+ * artifact-versus-runtime divergence that `.default()` publishing as
2547
+ * `required` produces, pointing the other way. The rule is enforced
2616
2548
  * where the correlation is used, in the cloud's bridge frame handler, and
2617
2549
  * stated here so nobody has to derive it from that code.
2618
2550
  */
2619
2551
  request_id: import_zod8.z.string().min(1).max(64).nullable()
2620
2552
  });
2621
2553
 
2622
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/config-issues.js
2554
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/config-issues.js
2623
2555
  var EXPOSURE_SECTIONS = ["datapoints", "actions", "services", "publishers", "cameras"];
2624
2556
  var EXPOSURE_SECTION_NAMES = new Set(EXPOSURE_SECTIONS);
2625
2557
 
2626
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/rest.js
2558
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/rest.js
2627
2559
  var import_zod9 = require("zod");
2628
2560
  var robot = import_zod9.z.object({
2629
2561
  id: import_zod9.z.uuid().meta({
@@ -2763,7 +2695,7 @@ var invokeRequest = import_zod9.z.object({
2763
2695
  description: "The values this call needs, keyed by **parameter name** rather than by field path \u2014 so a name survives the field moving inside the message. Every parameter without a default must be present, and the bounds the configuration declares are enforced in the cloud, before anything reaches the robot."
2764
2696
  }),
2765
2697
  /**
2766
- * How long **this call** is worth waiting for, in milliseconds (W6b).
2698
+ * How long **this call** is worth waiting for, in milliseconds.
2767
2699
  *
2768
2700
  * **Absent means `DEFAULT_PATIENCE_MS`** — today's behaviour, unchanged, for
2769
2701
  * every caller who does not care. It is optional because most callers have
@@ -2878,16 +2810,14 @@ var cameraListResponse = import_zod9.z.object({
2878
2810
  });
2879
2811
  var liveSessionResponse = import_zod9.z.object({
2880
2812
  /**
2881
- * This viewer's hold, and the **only** thing `DELETE` should be given
2882
- * (W6b).
2813
+ * This viewer's hold, and the **only** thing `DELETE` should be given.
2883
2814
  *
2884
- * A hold was addressed by `{identity, robot, slug}` and nothing else, so
2885
- * two tabs of one logged-in user were one hold as far as the refcount could
2886
- * see. Closing either tab released it: the second tab kept its LiveKit
2887
- * connection — the token is checked at join and never again — and went on
2888
- * rendering a video that the robot had already stopped producing. The
2889
- * viewer sees a frozen picture, not an ended session, which is the failure
2890
- * this project rejects everywhere else.
2815
+ * A hold addressed by `{identity, robot, slug}` alone would make two tabs of
2816
+ * one logged-in user a single hold as far as the refcount can see. Closing
2817
+ * either tab would release it, and the surviving tab would keep its LiveKit
2818
+ * connection — the token is checked at join and never again — rendering a
2819
+ * video the robot had already stopped producing. A frozen picture is not an
2820
+ * ended session.
2891
2821
  *
2892
2822
  * `DELETE` without a session id keeps today's meaning — *release my holds
2893
2823
  * on this camera* — because an SDK that has lost its id, or a client that
@@ -2945,26 +2875,23 @@ var historyQuery = import_zod9.z.object({
2945
2875
  description: "A dotted path to a numeric field inside an object value, such as `pose.x`. Without it the datapoint's value is used whole, which only works when it is already a number."
2946
2876
  }),
2947
2877
  /**
2948
- * **A union whose input branch IS the wire, not a coercion (W9d, DEF-059).**
2878
+ * **A union whose input branch IS the wire, not a coercion.**
2949
2879
  *
2950
- * This was `z.coerce.number()`, for a good reason that stayed true: the
2951
- * schema describes a **query string**, where every value arrives as text,
2952
- * and a bare `z.number()` would make each route coerce by hand. What was
2953
- * measured afterwards is that a coercion cannot be *published*: zod renders
2954
- * a coercion's **result** in either `io` mode, so `io: 'input'` and
2955
- * `io: 'output'` both emit `{"type":"integer"}` — an artifact describing a
2880
+ * The schema describes a **query string**, where every value arrives as
2881
+ * text. `z.coerce.number()` would read it, but a coercion cannot be
2882
+ * *published*: zod renders a coercion's **result** in either `io` mode, so
2883
+ * input and output both emit `{"type":"integer"}` — an artifact describing a
2956
2884
  * shape a query string can never carry. Anyone validating a real request
2957
2885
  * against it rejects every one that sets `limit`.
2958
2886
  *
2959
- * That is a **different** defect from the `.default()` class, which
2960
- * `io: 'input'` genuinely does fix; `export-schemas.ts` once claimed one
2961
- * remedy for both and has been corrected.
2887
+ * That is a different problem from `.default()` publishing as required,
2888
+ * which input-mode export genuinely does fix.
2962
2889
  *
2963
- * A union states both truths honestly: the wire carries a numeric string,
2964
- * a programmatic caller may pass a number, and the artifact can render the
2890
+ * A union states both truths honestly: the wire carries a numeric string, a
2891
+ * programmatic caller may pass a number, and the artifact can render the
2965
2892
  * input branch because there is one to render.
2966
2893
  *
2967
- * **What the artifact no longer says, named here rather than left silent.**
2894
+ * **What the artifact does not say, named here rather than left silent.**
2968
2895
  * The `1..10000` bound lives in the `.pipe()`, which is the *output* half, so
2969
2896
  * no input-mode artifact can express it as a constraint: the published shape
2970
2897
  * is `^\d{1,5}$` or a bare integer, and five digits is a weak echo of the
@@ -2972,11 +2899,10 @@ var historyQuery = import_zod9.z.object({
2972
2899
  * parsing, not by the shape of the text — but it is a **reduction**, and an
2973
2900
  * artifact that stops naming a bound reads as if there were none.
2974
2901
  *
2975
- * So both branches carry the number in a `.describe()` (Nimbus-W9d's
2976
- * proposal). It is **not** a constraint and nothing validates against it; it
2977
- * means a generator, or a person reading only the published schema, sees the
2978
- * actual ceiling instead of nothing. The gap is narrowed and named rather
2979
- * than closed.
2902
+ * So both branches carry the number in a `.describe()`. It is **not** a
2903
+ * constraint and nothing validates against it; it means a generator, or a
2904
+ * person reading only the published schema, sees the actual ceiling instead
2905
+ * of nothing. The gap is narrowed and named rather than closed.
2980
2906
  */
2981
2907
  limit: import_zod9.z.union([
2982
2908
  import_zod9.z.string().regex(/^\d{1,5}$/).describe("Positive integer, 1-10000. The pattern only bounds digit count; the real ceiling is enforced after parsing."),
@@ -3097,8 +3023,7 @@ var robotDeletionSummary = import_zod9.z.object({
3097
3023
  * console renders them in one sentence: *"this deletes N published slugs …
3098
3024
  * and M cameras"*. With cameras inside `slug_count` that sentence counts
3099
3025
  * them twice, on the one screen whose whole justification is naming what an
3100
- * irreversible click destroys (Momus, W6a review — the cloud summed all
3101
- * five and the console then added the cameras again).
3026
+ * irreversible click destroys.
3102
3027
  *
3103
3028
  * A draft is destroyed too and is described by `had_unpublished_draft`
3104
3029
  * rather than by either of these: describing three things with two numbers
@@ -3109,7 +3034,7 @@ var robotDeletionSummary = import_zod9.z.object({
3109
3034
  bytes_freed: import_zod9.z.number().int().nonnegative(),
3110
3035
  cameras: import_zod9.z.array(slug),
3111
3036
  /**
3112
- * Assets destroyed with the robot (W7), and **`asset_bytes_freed` is what
3037
+ * Assets destroyed with the robot, and **`asset_bytes_freed` is what
3113
3038
  * this org actually gets back** — not the sum of the assets' sizes.
3114
3039
  *
3115
3040
  * Storage is content-addressed, so a mesh two robots share survives the
@@ -3183,7 +3108,7 @@ var RESOURCE_HEALTH_STATES = [
3183
3108
  "unreadable_credential",
3184
3109
  /**
3185
3110
  * A camera names a credential that **does not exist** in this org — deleted,
3186
- * mistyped, or belonging to somebody else (W6a review).
3111
+ * mistyped, or belonging to somebody else.
3187
3112
  *
3188
3113
  * Separate from `unreadable_credential` because that one asserts a
3189
3114
  * decryption that was attempted and failed, and here nothing was ever
@@ -3193,11 +3118,11 @@ var RESOURCE_HEALTH_STATES = [
3193
3118
  * — a different fact with a different fix.
3194
3119
  *
3195
3120
  * **Retiring with the credential store**, and not live behaviour to build
3196
- * against. Its one producer was `cloud-config-frame.ts` tolerating an
3197
- * unresolved `credentials_ref` at publish time; FL-002 deleted that field,
3198
- * so nothing emits this today. It is kept only until the wave that removes
3199
- * the store also removes these three credential states — `unreadable_credential`
3200
- * and the `readable` fact on `credentialSummary` go the same way.
3121
+ * against. Its one producer tolerated an unresolved `credentials_ref` at
3122
+ * publish time; that field is gone, so nothing emits this today. It is kept
3123
+ * only until the credential store is removed, which takes these three
3124
+ * credential states with it — `unreadable_credential` and the `readable` fact
3125
+ * on `credentialSummary` go the same way.
3201
3126
  */
3202
3127
  "credential_missing",
3203
3128
  /** A configuration change stopped this stream, deliberately. */
@@ -3222,18 +3147,17 @@ var resourceHealthState = import_zod9.z.object({
3222
3147
  /** The camera slug, or the credential name. */
3223
3148
  ref: import_zod9.z.string().min(1).max(64),
3224
3149
  /**
3225
- * **Which of two questions this entry answers (W9a, DEF-072).**
3150
+ * **Which of two questions this entry answers.**
3226
3151
  *
3227
3152
  * `'source'` — can the source be read at all? (`unreachable`, `auth_failed`,
3228
3153
  * `unreadable_credential`, `missing_credential`, `ok`, …)
3229
3154
  * `'publish'` — given a readable source, did publishing to LiveKit work?
3230
3155
  *
3231
- * Before this, both went into one entry keyed `${robot} ${kind} ${ref}` with
3232
- * one flat `state`, in which `publish_failed` answered *"can we publish"*
3233
- * and every other value answered *"can the source be read"* — **same key,
3234
- * same field, two questions**, so each overwrote the other. The conflation
3235
- * was once an occasional race; W6a's reconnect restatement made it
3236
- * guaranteed, on every reconnect, for any camera with an active viewer.
3156
+ * Without the facet both answers land in one entry keyed
3157
+ * `${robot} ${kind} ${ref}` with one flat `state`, in which `publish_failed`
3158
+ * answers *"can we publish"* and every other value answers *"can the source
3159
+ * be read"* — same key, same field, two questions, each overwriting the
3160
+ * other.
3237
3161
  *
3238
3162
  * The facet is part of the entry's identity: a camera can perfectly well be
3239
3163
  * readable and unpublishable at the same moment, and that pair is exactly
@@ -3266,30 +3190,28 @@ var orgQuotas = import_zod9.z.object({
3266
3190
  max_retention_writes_per_minute: import_zod9.z.number().int().nonnegative(),
3267
3191
  max_realtime_connections: import_zod9.z.number().int().positive(),
3268
3192
  /**
3269
- * Asset storage (§4.6, W7) — **its own dial, not part of
3270
- * `max_retention_bytes`.** A sync grows storage in jumps and time series
3271
- * grow steadily; one dial would let the first crowd out the second, and the
3272
- * org that hit its limit would be told to look at the wrong thing.
3193
+ * Asset storage — **its own dial, not part of `max_retention_bytes`.** A
3194
+ * sync grows storage in jumps and time series grow steadily; one dial would
3195
+ * let the first crowd out the second, and the org that hit its limit would be
3196
+ * told to look at the wrong thing.
3273
3197
  *
3274
3198
  * **Counted per distinct blob *this org references* — not per asset row, and
3275
- * not per object the platform stores on its behalf (W7a, D1).** The two
3276
- * readings are indistinguishable from the number alone and a customer is
3277
- * entitled to know which one they are being charged for.
3199
+ * not per object the platform stores on its behalf.** The two readings are
3200
+ * indistinguishable from the number alone and a customer is entitled to know
3201
+ * which one they are being charged for.
3278
3202
  *
3279
3203
  * Within an org, sharing is free: two robots referencing the same mesh cost
3280
3204
  * one copy, which is what dedup means to a customer, and anything else
3281
3205
  * charges an org twice for a fleet of identical robots — the normal case.
3282
3206
  *
3283
- * **Across orgs, sharing is not free, and W7 shipped the opposite.** Storage
3284
- * stays globally content-addressed (one object per sha256; that efficiency
3285
- * is real), but accounting is per-org: an org is charged for each distinct
3286
- * blob it references and credited when its own last reference goes, whether
3287
- * or not the blob survives for somebody else. Global refcounting made the
3288
- * first org to sync a blob pay for it forever while every later org stored
3289
- * it free — so the quota was evadable by anyone whose mesh someone else had
3290
- * already uploaded, and an org's own number depended on who got there first,
3291
- * which nobody can predict. Measured before the change: 342 bytes held by an
3292
- * org owning no assets, with no operation able to free them.
3207
+ * **Across orgs, sharing is not free.** Storage stays globally
3208
+ * content-addressed (one object per sha256; that efficiency is real), but
3209
+ * accounting is per-org: an org is charged for each distinct blob it
3210
+ * references and credited when its own last reference goes, whether or not
3211
+ * the blob survives for somebody else. Global refcounting would make the
3212
+ * first org to sync a blob pay for it forever while every later org stored it
3213
+ * free — a quota evadable by anyone whose mesh someone else had already
3214
+ * uploaded, and an org's own number would depend on who got there first.
3293
3215
  */
3294
3216
  max_asset_storage_bytes: import_zod9.z.number().int().nonnegative()
3295
3217
  });
@@ -3333,7 +3255,7 @@ var robotLatencySeries = import_zod9.z.object({
3333
3255
  });
3334
3256
  var orgLatencyQuery = import_zod9.z.object({
3335
3257
  from_ms: wireTimestampMs,
3336
- /** Exclusive — half-open `[from, to)`, the convention every other query here already follows (DEF-062). */
3258
+ /** Exclusive — half-open `[from, to)`, the convention every other query here already follows. */
3337
3259
  to_ms: wireTimestampMs,
3338
3260
  /**
3339
3261
  * One robot's own sparkline. `z.uuid()`, because the column is one —
@@ -3393,13 +3315,13 @@ var slugUsageResponse = import_zod9.z.object({
3393
3315
  alert_count: import_zod9.z.number().int().nonnegative()
3394
3316
  });
3395
3317
 
3396
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/realtime.js
3318
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/realtime.js
3397
3319
  var import_zod14 = require("zod");
3398
3320
 
3399
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/client-auth.js
3321
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/client-auth.js
3400
3322
  var import_zod13 = require("zod");
3401
3323
 
3402
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/apps.js
3324
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/apps.js
3403
3325
  var import_zod10 = require("zod");
3404
3326
  var appIdentifier = slug;
3405
3327
  var app = import_zod10.z.object({
@@ -3415,7 +3337,7 @@ var app = import_zod10.z.object({
3415
3337
  identifier: appIdentifier.meta({
3416
3338
  description: "The stable handle a client sends at login, lowercase and underscore-separated. **Globally unique, not per organisation** \u2014 `clientLoginRequest` carries no org context to disambiguate with, so a collision is refused with `identifier_taken`."
3417
3339
  }),
3418
- /** Robots are referenced individually; tags never grant rights (§12.2). */
3340
+ /** Robots are referenced individually; tags never grant rights. */
3419
3341
  robot_ids: import_zod10.z.array(import_zod10.z.uuid()).meta({
3420
3342
  description: "The robots this app may reach, each referenced individually. Tags never grant rights, and a robot absent from this list is invisible to the app whatever a role grants."
3421
3343
  }),
@@ -3468,16 +3390,17 @@ var updateAppRequest = import_zod10.z.object({
3468
3390
  name: import_zod10.z.string().min(1).max(120).optional(),
3469
3391
  robot_ids: import_zod10.z.array(import_zod10.z.uuid()).optional(),
3470
3392
  /**
3471
- * `app.default_role_id`'s write half — an app *setting*, which is where D1
3472
- * put the default role, so it belongs on the app's own PATCH and not on a
3473
- * route of its own.
3474
- *
3475
- * **`.nullable().optional()`, and the two mean different things.** Absent
3476
- * leaves the current default alone; an explicit `null` clears it. A field
3477
- * that could only be set and never unset would make "we changed our mind"
3478
- * unreachable through the API — the same silence `.strict()` above exists to
3479
- * avoid, from the other direction.
3480
- */
3393
+ * `app.default_role_id`'s write half — an app *setting*, which is where the
3394
+ * two-identity-space model
3395
+ * put the default role, so it belongs on the app's own PATCH and not on a
3396
+ * route of its own.
3397
+ *
3398
+ * **`.nullable().optional()`, and the two mean different things.** Absent
3399
+ * leaves the current default alone; an explicit `null` clears it. A field
3400
+ * that could only be set and never unset would make "we changed our mind"
3401
+ * unreachable through the API — the same silence `.strict()` above exists to
3402
+ * avoid, from the other direction.
3403
+ */
3481
3404
  default_role_id: import_zod10.z.uuid().nullable().optional()
3482
3405
  }).strict();
3483
3406
  var serverKeyToken = import_zod10.z.string().regex(/^flk_[0-9a-f]{32}$/);
@@ -3530,16 +3453,13 @@ var roleListResponse = import_zod10.z.object({
3530
3453
  var rolePermissions = import_zod10.z.object({
3531
3454
  role_id: import_zod10.z.uuid(),
3532
3455
  /**
3533
- * **A slug is unique per robot across ALL service kinds** (spec §4.1:
3534
- * "Jeder Dienst erhält einen Slug" — one namespace, not one per kind), and
3535
- * the cloud's config validation enforces that with a kind-agnostic
3536
- * collection pass. That is why this list carries slugs and not
3537
- * (kind, slug) pairs: when W4 adds actions, services and publishers, a
3538
- * grant keeps meaning exactly what it means today, and this shape does not
3539
- * change. What W4 does need is an endpoint that lists every *grantable*
3540
- * slug of a robot with its kind, so the console's matrix can offer them —
3541
- * today it enumerates datapoints only, which is the seam that would
3542
- * otherwise force a rebuild.
3456
+ * **A slug is unique per robot across ALL exposure kinds** — one namespace,
3457
+ * not one per kind — and the cloud's configuration validation enforces that
3458
+ * with a kind-agnostic collection pass. That is why this list carries slugs
3459
+ * and not (kind, slug) pairs: a grant means the same thing whichever kind the
3460
+ * slug turns out to name. `GET /api/robots/:id/exposures` is the companion
3461
+ * read that lists every grantable slug of a robot **with** its kind, so a
3462
+ * rights matrix can offer them.
3543
3463
  */
3544
3464
  grants: import_zod10.z.array(import_zod10.z.object({
3545
3465
  robot_id: import_zod10.z.uuid(),
@@ -3548,25 +3468,22 @@ var rolePermissions = import_zod10.z.object({
3548
3468
  /**
3549
3469
  * App-wide abilities a role grants, as opposed to per-slug grants above.
3550
3470
  *
3551
- * **A capability here is a promise, and one of them is still not kept.**
3552
- * `action_history` and `presence` were both gated by this object from W4
3553
- * and implemented nowhere — no route, no SDK method, no realtime frame
3554
- * (register row 8). A console could therefore switch them on and nothing
3555
- * changed, which is worse than their absence: the developer believes they
3556
- * granted something. **This paragraph stays** whatever the current tally
3557
- * is: it is the only place that says a switch in the console may change
3558
- * nothing, and it is how the next unkept capability gets caught.
3471
+ * **A capability here is a promise, and one of them is still not kept.** A
3472
+ * capability with no route, no SDK method and no realtime frame behind it can
3473
+ * be switched on while nothing changes, which is worse than its absence: the
3474
+ * developer believes they granted something. **This paragraph stays**
3475
+ * whatever the current tally is: it is the only place that says a switch may
3476
+ * change nothing, and it is how the next unkept capability gets caught.
3559
3477
  *
3560
- * `assets` (W7) was the first one redeemed. It gates §4.6's asset store,
3561
- * which is not covered by `grants` because **assets are not slugs** — and it
3562
- * is its own decision rather than a side effect of reaching the robot,
3563
- * because a mesh set gives away the machine's build.
3478
+ * `assets` gates the asset store, which is not covered by `grants` because
3479
+ * **assets are not slugs** — and it is its own decision rather than a side
3480
+ * effect of reaching the robot, because a mesh set gives away the machine's
3481
+ * build.
3564
3482
  *
3565
- * **`action_history` is kept as of the run-history delta.** It gates
3483
+ * **`action_history` is kept.** It gates
3566
3484
  * `GET /api/robots/:id/jobs/history` — an end user whose role lacks it is
3567
3485
  * refused `403 capability_required`, naming the capability so the developer
3568
- * knows which switch is off. It was unkeepable while nothing durable
3569
- * recorded what had run; `jobRun` and `job_runs` are that record.
3486
+ * knows which switch is off.
3570
3487
  *
3571
3488
  * **What granting it discloses.** A `jobRun` names the actor who invoked
3572
3489
  * it, and `jobActor.label` is an email — so an end user holding this
@@ -3586,10 +3503,10 @@ var rolePermissions = import_zod10.z.object({
3586
3503
  })
3587
3504
  });
3588
3505
 
3589
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/app-users.js
3506
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/app-users.js
3590
3507
  var import_zod12 = require("zod");
3591
3508
 
3592
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/identity.js
3509
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/identity.js
3593
3510
  var import_zod11 = require("zod");
3594
3511
  var password = import_zod11.z.string().min(12).max(256);
3595
3512
  var USER_DISPLAY_NAME_MAX = 120;
@@ -3751,7 +3668,7 @@ var authMeResponse = import_zod11.z.object({ org, user: fleetlessUser });
3751
3668
  var patchOrgRequest = import_zod11.z.object({ name: import_zod11.z.string().min(1).max(120) }).strict();
3752
3669
  var patchAuthMeRequest = import_zod11.z.object({ display_name: import_zod11.z.string().min(1).max(USER_DISPLAY_NAME_MAX).nullable() }).strict();
3753
3670
 
3754
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/app-users.js
3671
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/app-users.js
3755
3672
  var APP_USER_DISPLAY_NAME_MAX = 120;
3756
3673
  var providerSlug = import_zod12.z.string().max(40).regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, "a provider slug is lowercase and hyphen-separated, starting with a letter");
3757
3674
  var appUserStatus = import_zod12.z.enum(["pending_verification", "active", "blocked"]);
@@ -3900,7 +3817,7 @@ var createAppOidcProviderRequest = import_zod12.z.object({
3900
3817
  description: "The client secret, **write-only**: it is stored encrypted and comes back through nothing \u2014 not the read, not this route's own answer, not an audit detail. Required on create, since a provider with no secret cannot exchange a code; the minimum length refuses a value that is a misconfiguration rather than a secret."
3901
3818
  }),
3902
3819
  scopes: import_zod12.z.array(import_zod12.z.string().min(1).max(60)).min(1).max(20).default(["openid", "email", "profile"]).meta({
3903
- description: "The scopes to request. Defaults to `openid email profile`, which is what the linking rules in this design actually read: the subject, the address and its verified flag, and a name."
3820
+ description: "The scopes to request. Defaults to `openid email profile`, which is what account linking actually reads: the subject, the address and its verified flag, and a name."
3904
3821
  }),
3905
3822
  link_verified_emails: import_zod12.z.boolean().default(false).meta({
3906
3823
  description: "Whether a federated login may join an existing app user by verified address. **Defaults to off**, because relaxing later is additive and admitting duplicates now and tightening afterwards is not."
@@ -4002,7 +3919,7 @@ var mailOutcome = import_zod12.z.object({
4002
3919
  })
4003
3920
  });
4004
3921
 
4005
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/client-auth.js
3922
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/client-auth.js
4006
3923
  var clientLoginRequest = import_zod13.z.object({
4007
3924
  app_identifier: appIdentifier.meta({
4008
3925
  description: "The app being logged in to, as its globally unique identifier \u2014 the lowercase, underscore-separated string the developer chose when the app was created. There is no organisation context at login, so this is what decides which app the credentials are checked for."
@@ -4140,7 +4057,7 @@ var clientMcpInteraction = import_zod13.z.object({
4140
4057
  description: "Always `false`. The client registered itself without authentication and chose this name about itself, so it must be rendered as a claim and never as an identity. There is no verified case, which is why this is a literal and not a boolean: a `true` branch would be dead code that looked like a safeguard."
4141
4058
  }),
4142
4059
  scopes: import_zod13.z.array(import_zod13.z.string()).meta({ description: "The scopes the client asked for, to show the person before they approve." }),
4143
- already_granted: import_zod13.z.boolean().meta({ description: "Whether this user has already approved this client. It is a record of what they answered last time and nothing more: **the server performs no second check of it**, so an app that skips its own consent screen when this is `true` is the only thing deciding, and approve succeeds identically for a user who holds no grant at all. Withdrawing it is `DELETE /api/client/mcp/grants/:clientId` for the person themselves and `DELETE /api/apps/:id/users/:userId/mcp-grants/:clientId` for the developer. A withdrawal makes this `false` again at the next authorization and does not end an MCP session already running: the access token it minted stays valid for the rest of its fifteen minutes, and no refresh grant exists to extend it." }),
4060
+ already_granted: import_zod13.z.boolean().meta({ description: "Whether this user has already approved this client. It is a record of what they answered last time, and **this route makes no second use of it**: an app that skips its own consent screen when this is `true` is the only thing deciding that, and approve succeeds identically for a user who holds no grant at all. The standing grant is read elsewhere, on every request to the app's MCP endpoint. Withdrawing it is `DELETE /api/client/mcp/grants/:clientId` for the person themselves and `DELETE /api/apps/:id/users/:userId/mcp-grants/:clientId` for the developer. A withdrawal makes this `false` again at the next authorization **and stops the client at its very next MCP call**, unexpired access token and all \u2014 up to fifteen minutes of it \u2014 because the endpoint keys that check on the `client_id` the token carries." }),
4144
4061
  expires_at: import_zod13.z.iso.datetime().meta({ description: "When the interaction stops being approvable. Ten minutes from the authorize step; afterwards both approve and deny answer `interaction_expired`." })
4145
4062
  });
4146
4063
  var clientMcpInteractionDecisionResponse = import_zod13.z.object({
@@ -4191,7 +4108,7 @@ var clientIdentity = import_zod13.z.object({
4191
4108
  })
4192
4109
  });
4193
4110
 
4194
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/realtime.js
4111
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/realtime.js
4195
4112
  var clientAuth = import_zod14.z.object({
4196
4113
  type: import_zod14.z.literal("auth"),
4197
4114
  token: import_zod14.z.string().min(1)
@@ -4210,20 +4127,16 @@ var clientInvoke = import_zod14.z.object({
4210
4127
  request_id: import_zod14.z.string().min(1).max(64),
4211
4128
  robot_id: import_zod14.z.uuid(),
4212
4129
  slug,
4213
- /** Parameters by field path, validated against the config's rules (§4.4). */
4130
+ /** Parameters by field path, validated against the configuration's rules. */
4214
4131
  params: import_zod14.z.record(import_zod14.z.string(), import_zod14.z.unknown()),
4215
4132
  /**
4216
- * How long this one call is worth waiting for (W6b) — the same field,
4217
- * meaning and cap as `invokeRequest.patience_ms`; absent means
4218
- * `DEFAULT_PATIENCE_MS`.
4133
+ * How long this one call is worth waiting for — the same field, meaning and
4134
+ * cap as `invokeRequest.patience_ms`; absent means `DEFAULT_PATIENCE_MS`.
4219
4135
  *
4220
- * It is here because **§11.1 parity is a rule, not a preference**: what REST
4221
- * can do travels over this socket. The first version of this delta gave
4222
- * `patience_ms` to the REST body only — and the SDK invokes exclusively over
4223
- * the realtime channel, so the field would have been unreachable for every
4224
- * SDK caller while appearing in the documentation. W6a shipped four SDK
4225
- * methods no SDK caller could invoke; this is the same defect caught before
4226
- * it shipped, by the SDK owner rather than by a reviewer.
4136
+ * It is here because **parity is a rule, not a preference**: what REST can do
4137
+ * travels over this socket. A field given to the REST body alone would be
4138
+ * unreachable to every caller that invokes over the realtime channel, while
4139
+ * still appearing in the documentation.
4227
4140
  */
4228
4141
  patience_ms: import_zod14.z.number().int().min(MIN_PATIENCE_MS).max(MAX_PATIENCE_MS).optional()
4229
4142
  });
@@ -4231,17 +4144,17 @@ var clientCancel = import_zod14.z.object({
4231
4144
  type: import_zod14.z.literal("cancel"),
4232
4145
  request_id: import_zod14.z.string().min(1).max(64),
4233
4146
  robot_id: import_zod14.z.uuid(),
4234
- /** Which slug — required, and the only address a cancel had until W6b. */
4147
+ /** Which slug — required, and the coarse address of a cancel. */
4235
4148
  slug,
4236
4149
  /**
4237
- * Which job on that slug (W6b), or `null` for *whatever is running there*.
4150
+ * Which job on that slug, or `null` for *whatever is running there*.
4238
4151
  *
4239
4152
  * The two are different requests and both are legitimate. An operator
4240
4153
  * hitting a stop button means the second: stop the machine, whatever it is
4241
4154
  * doing. A client cancelling the job it started means the first — and until
4242
- * this field existed it could not say so, so a cancel that arrived just
4243
- * after its own job ended stopped the next caller's job instead. Same slug,
4244
- * same wire frame, entirely different machine behaviour, and nothing in the
4155
+ * this field a client could not say so, and a cancel arriving just after its
4156
+ * own job ended would stop the next caller's job instead. Same slug, same
4157
+ * wire frame, entirely different machine behaviour, and nothing in the
4245
4158
  * protocol able to tell them apart.
4246
4159
  *
4247
4160
  * A named id that is not running answers `not_found` rather than falling
@@ -4267,9 +4180,9 @@ var commandResult = import_zod14.z.object({
4267
4180
  * - `ok:true` on an invoke or a call: the job that was just created.
4268
4181
  * - `ok:true` on a cancel: the job the cancel was sent to.
4269
4182
  * - `ok:false, code:'busy'`: **the job that is already running** — the
4270
- * caller has none. This is the §11.3 "inkl. Information, was läuft", and
4271
- * it is the whole reason a busy refusal is useful: the caller learns
4272
- * whether to wait or to give up (see `busyDetails`).
4183
+ * caller has none. Naming what is already running is the whole reason a
4184
+ * busy refusal is useful: the caller learns whether to wait or to give up
4185
+ * (see `busyDetails`).
4273
4186
  * - any other refusal: `null`.
4274
4187
  */
4275
4188
  job: job.nullable(),
@@ -4291,12 +4204,11 @@ var commandResult = import_zod14.z.object({
4291
4204
  * The same payload the REST envelope carries in `apiError.details` — for
4292
4205
  * `parameter_invalid`, a `parameterInvalidDetails`.
4293
4206
  *
4294
- * Added because it was missing, and its absence quietly broke §11.1: this
4295
- * socket is supposed to do *everything* REST can do, but a
4296
- * `parameter_invalid` arriving here had nowhere to put its violations, so
4297
- * the same refusal was actionable over HTTP and opaque over the socket.
4298
- * A client cannot bind an error to the input that caused it from a code
4299
- * alone — which is the entire point of the flat parameter shape.
4207
+ * It is here because this socket does *everything* REST can do, and without
4208
+ * it a `parameter_invalid` arriving here would have nowhere to put its
4209
+ * violations — the same refusal actionable over HTTP and opaque over the
4210
+ * socket. A client cannot bind an error to the input that caused it from a
4211
+ * code alone, which is the entire point of the flat parameter shape.
4300
4212
  */
4301
4213
  details: import_zod14.z.unknown().optional()
4302
4214
  });
@@ -4310,7 +4222,7 @@ var clientSubscribe = import_zod14.z.object({
4310
4222
  robot_id: import_zod14.z.uuid(),
4311
4223
  slug,
4312
4224
  /**
4313
- * What the subscriber expects, and how it wants it (W5).
4225
+ * What the subscriber expects, and how it wants it.
4314
4226
  *
4315
4227
  * `kind` lets the server answer **`wrong_kind`** instead of accepting a
4316
4228
  * subscribe the client will then filter to silence — and silence is
@@ -4318,8 +4230,6 @@ var clientSubscribe = import_zod14.z.object({
4318
4230
  * Optional, so an older client that omits it keeps today's behaviour.
4319
4231
  *
4320
4232
  * `options` is where a camera says what it wants; a datapoint needs none.
4321
- * It exists now rather than later because adding a field to a frame three
4322
- * repos parse is cheap once and expensive twice.
4323
4233
  */
4324
4234
  /**
4325
4235
  * `publisher` is here even though a publisher is not subscribable: a client
@@ -4369,7 +4279,7 @@ var liveSessionEndReason = import_zod14.z.enum([
4369
4279
  /**
4370
4280
  * The cloud ended it and cannot say which of the above applied. **Kept
4371
4281
  * deliberately**: a channel that cannot say "I do not know" will say
4372
- * something false instead, and this project has paid for that four times in
4282
+ * something false instead, which is the costlier failure in
4373
4283
  * the camera path alone.
4374
4284
  */
4375
4285
  "unknown"
@@ -4384,13 +4294,10 @@ var liveSessionEvent = import_zod14.z.object({
4384
4294
  /**
4385
4295
  * **Classified text the cloud produced, never text the robot sent.**
4386
4296
  *
4387
- * An earlier draft of this comment said *"the robot's own words when it has
4388
- * any"*, which reads as permission to pass `bridgeCameraState.error.message`
4389
- * straight through. Nothing sanitises that field, and this codebase has a
4390
- * documented incident of a password reaching a developer surface through
4391
- * exactly that route — `camera-health.ts`'s fixed-string `REASON` discipline
4392
- * exists because of it. Nimbus-W9a stopped at the sentence and asked rather
4393
- * than taking the permission it appeared to give (2026-08-19).
4297
+ * It is **not** the robot's own words. Nothing sanitises
4298
+ * `bridgeCameraState.error.message`, and a camera password reaches a
4299
+ * developer surface through exactly that route — which is why the cloud maps
4300
+ * a robot's diagnosis to fixed strings rather than forwarding it.
4394
4301
  *
4395
4302
  * So: `null` unless the cloud itself has something classified to say. If a
4396
4303
  * developer needs the robot's own diagnosis later, it arrives as a mapped
@@ -4479,7 +4386,7 @@ var orgEventDropped = import_zod14.z.object({
4479
4386
  dropped: import_zod14.z.number().int().positive()
4480
4387
  }).strict();
4481
4388
 
4482
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/audit.js
4389
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/audit.js
4483
4390
  var import_zod15 = require("zod");
4484
4391
  var auditActor = import_zod15.z.object({
4485
4392
  kind: import_zod15.z.enum(["developer", "end_user", "app_user", "server_key", "bridge"]),
@@ -4491,8 +4398,7 @@ var auditEvent = import_zod15.z.object({
4491
4398
  org_id: import_zod15.z.uuid(),
4492
4399
  at: import_zod15.z.iso.datetime(),
4493
4400
  /**
4494
- * A monotonic counter, ascending in write order, unique across the log
4495
- * (W6b).
4401
+ * A monotonic counter, ascending in write order, unique across the log.
4496
4402
  *
4497
4403
  * `at` is not a total order. Two events written in the same millisecond —
4498
4404
  * a login and the config publish it enables, a cascade writing several
@@ -4504,11 +4410,7 @@ var auditEvent = import_zod15.z.object({
4504
4410
  *
4505
4411
  * It is also the only correct **cursor** for paging this log, for the same
4506
4412
  * reason: a cursor that is not unique either skips rows or repeats them at
4507
- * every page boundary. No cursor parameter exists on `GET /api/audit` yet —
4508
- * the route returns the whole log — and that is stated here rather than
4509
- * implied, because a contract that describes a capability the API does not
4510
- * have is the defect this project keeps finding. When paging is added it
4511
- * uses this field; nothing else in this shape can carry it.
4413
+ * every page boundary. Nothing else in this shape can carry one.
4512
4414
  *
4513
4415
  * Required, not optional: an event without a sequence cannot be ordered
4514
4416
  * against one that has it, and a log with two orderings has none.
@@ -4545,7 +4447,7 @@ var auditQuery = import_zod15.z.object({
4545
4447
  /** Only events with a smaller `seq` — the next, older page. */
4546
4448
  before_seq: wireSeqCursor.optional(),
4547
4449
  /**
4548
- * Same shape as DEF-059's `historyQuery.limit`: a union whose input branch
4450
+ * The same shape as `historyQuery.limit`: a union whose input branch
4549
4451
  * **is the wire**. A `z.coerce` cannot be published — zod renders the
4550
4452
  * coercion's result in either `io` direction, so the artifact would describe
4551
4453
  * a shape a query string can never carry.
@@ -4574,36 +4476,25 @@ var auditQuery = import_zod15.z.object({
4574
4476
  /**
4575
4477
  * Only events by this actor.
4576
4478
  *
4577
- * **`z.uuid()`, because the column is one (Argus-W9, W9 review).** This was
4578
- * `z.string().min(1).max(200)`, so any non-uuid value reached Postgres as a
4579
- * uuid parameter and threw: `?actor_id=not-a-uuid` answered **500
4580
- * `internal_error`**, on the list route and the export alike.
4479
+ * **`z.uuid()`, because the column is one.** A looser string type lets any
4480
+ * non-uuid value reach the database as a uuid parameter, where the cast
4481
+ * throws: `?actor_id=not-a-uuid` then answers **500 `internal_error`** rather
4482
+ * than refusing the value.
4581
4483
  *
4582
- * Not a SQL-injection finding — Drizzle parameterises, and `' or 1=1--`
4583
- * failed at the same cast. It is a **500 where a 400 belongs**, and a 500 is
4584
- * the answer that explains nothing.
4585
- *
4586
- * The place is the part worth keeping: **this same wave pulled
4587
- * `refuseIfNotUuid` through ~15 call sites** so a typo could be told from a
4588
- * deletion — and the brand-new filter, whose field has exactly that shape,
4589
- * is the one that did not get it. A rule applied to the sites in front of
4590
- * you is not a rule applied to the class.
4484
+ * Not an injection question — the query is parameterised either way. It is a
4485
+ * **500 where a 400 belongs**, and a 500 is the answer that explains nothing.
4591
4486
  */
4592
4487
  actor_id: import_zod15.z.uuid().optional(),
4593
4488
  /** Only events about this kind of target, e.g. `robot`. */
4594
4489
  target_kind: import_zod15.z.string().min(1).max(40).optional(),
4595
4490
  /**
4596
4491
  * Absolute bounds in unix milliseconds, **half-open `[from, to)`** — the
4597
- * same rule the history shapes follow (DEF-062).
4492
+ * same rule the history shapes follow.
4598
4493
  *
4599
- * **Bounded to years 1..9999, and the bound is borrowed rather than
4600
- * invented.** `nonnegative()` alone let `253402300800000` (year 10000)
4601
- * through, where the Postgres bind path has no representation and the route
4602
- * answered 500 — measured either side of the edge: `253402300799000` → 200,
4603
- * `253402300800000` → 500 (Argus-W9). `history-query.ts`'s `parseTimeExprMs`
4604
- * already carries exactly this range, with M3's reasoning for why
4605
- * `Number.isSafeInteger` is wider than what a timestamp can be; this is that
4606
- * same number, not a second one that happens to agree.
4494
+ * **Bounded to years 1..9999.** `nonnegative()` alone admits instants a
4495
+ * timestamp column has no representation for, and the route answers 500
4496
+ * rather than refusing the value. `Number.isSafeInteger` is wider than what a
4497
+ * timestamp can be, so the bound is stated rather than inherited.
4607
4498
  */
4608
4499
  from_ms: auditTimestampMs.optional(),
4609
4500
  to_ms: auditTimestampMs.optional()
@@ -4626,7 +4517,7 @@ var auditListResponse = import_zod15.z.object({
4626
4517
  next_cursor: import_zod15.z.number().int().positive().nullable()
4627
4518
  });
4628
4519
 
4629
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/errors.js
4520
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/errors.js
4630
4521
  var import_zod16 = require("zod");
4631
4522
  var apiError = import_zod16.z.object({
4632
4523
  code: import_zod16.z.string().min(1),
@@ -4643,7 +4534,7 @@ var parameterInvalidDetails = import_zod16.z.object({
4643
4534
  violations: import_zod16.z.array(parameterViolation).min(1)
4644
4535
  });
4645
4536
 
4646
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/oauth.js
4537
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/oauth.js
4647
4538
  var import_zod17 = require("zod");
4648
4539
  var oauthErrorCode = import_zod17.z.enum([
4649
4540
  "invalid_request",
@@ -4673,8 +4564,8 @@ var oauthError = import_zod17.z.object({
4673
4564
  * makes the two indistinguishable to the caller, and *a field that cannot
4674
4565
  * express a distinction produces a workaround somewhere else*. Answering in
4675
4566
  * `apiError` instead would keep the distinction and hand an RFC-compliant
4676
- * client a body it cannot parse — which is the conformance this wave exists
4677
- * to provide.
4567
+ * client a body it cannot parse, which is the conformance this dialect
4568
+ * exists to provide.
4678
4569
  *
4679
4570
  * So both: `error` is what a standard client reads, `fleetless_code` is what
4680
4571
  * our own tooling switches on. RFC 6749 §5.2 permits additional members, and
@@ -4698,26 +4589,29 @@ var redirectUri = import_zod17.z.string().min(1).max(2e3).refine((v) => {
4698
4589
  return false;
4699
4590
  }, { message: "redirect_uri must be an https URL, or http on an explicit loopback address, and carry no fragment" });
4700
4591
  var codeChallengeMethod = import_zod17.z.enum(["S256"]);
4592
+ var MCP_DCR_MAX_REDIRECT_URIS = 5;
4701
4593
  var dynamicClientRegistrationRequest = import_zod17.z.object({
4702
- client_name: import_zod17.z.string().min(1).max(200).meta({
4703
- description: 'The name the client calls itself. It is **not** vouched for by Fleetless and must never be rendered as if it were \u2014 a self-registered client chooses this string, and one has called itself *"Fleetless Official Helper"*.'
4594
+ redirect_uris: import_zod17.z.array(redirectUri).min(1).max(MCP_DCR_MAX_REDIRECT_URIS).meta({
4595
+ description: `Where the authorization code may be returned, and the one field a registration cannot omit. Each must be an \`https\` URL, or \`http\` on an explicit loopback address for a native app that cannot hold a certificate, and none may carry a fragment. There must be between \`1\` and \`${MCP_DCR_MAX_REDIRECT_URIS}\` of them; duplicates are collapsed rather than counted twice. Matched **exactly** at the authorize step against what was registered here.`
4704
4596
  }),
4705
- redirect_uris: import_zod17.z.array(redirectUri).min(1).max(20).meta({
4706
- description: "Where the authorization code may be returned. Each must be an `https` URL, or `http` on an explicit loopback address for a native app that cannot hold a certificate, and none may carry a fragment. There must be between `1` and `20` of them."
4597
+ client_name: import_zod17.z.string().min(1).max(200).optional().meta({
4598
+ description: `The name the client calls itself. Optional \u2014 a registration without one is recorded under a default name, per RFC 7591's making every metadata field optional. It is **not** vouched for by Fleetless and must never be rendered as if it were: a self-registered client chooses this string, and one has called itself *"Fleetless Official Helper"*.`
4599
+ }),
4600
+ token_endpoint_auth_method: import_zod17.z.enum(["none"]).optional().meta({
4601
+ description: "`none`, RFC 7591's value for a public client, and the only value either server registers. Any other value is **refused rather than silently downgraded**: a client that believes it holds a secret and does not has a wrong mental model of its own security. There is no client secret to hold \u2014 mandatory PKCE (`S256`) is the defence."
4707
4602
  }),
4708
4603
  grant_types: import_zod17.z.array(import_zod17.z.enum(["authorization_code", "refresh_token"])).optional().meta({
4709
- description: "Accepted and echoed back for conformance with RFC 7591. This server issues `authorization_code` and `refresh_token` and nothing else."
4604
+ description: "Accepted for conformance with RFC 7591 and then **ignored**. What comes back is what was actually granted, which \xA73.2.1 permits a server to substitute: `authorization_code` and nothing else, so a client that asks for `refresh_token` is registered and told plainly that it did not get one."
4710
4605
  }),
4711
4606
  response_types: import_zod17.z.array(import_zod17.z.enum(["code"])).optional().meta({
4712
- description: "Accepted and echoed back for conformance. `code` is the only response type OAuth 2.1 leaves, the implicit grant having been removed."
4713
- }),
4714
- token_endpoint_auth_method: import_zod17.z.enum(["none"]).optional().meta({
4715
- description: "`none`, RFC 7591's value for a public client. There is no client secret to hold: mandatory PKCE is the defence."
4607
+ description: "Accepted for conformance and then **ignored**; the response names `code`, which is the only response type OAuth 2.1 leaves, the implicit grant having been removed."
4716
4608
  }),
4717
4609
  scope: import_zod17.z.string().max(500).optional().meta({
4718
- description: "The scopes the client asks to be registered for, space-separated."
4610
+ description: "Accepted for conformance and then **ignored**. This authorization server issues no scopes at all, which is why the registration answer carries no `scope` field to echo one back in."
4719
4611
  })
4720
- }).strict();
4612
+ }).meta({
4613
+ description: "What an MCP client sends to register itself, per RFC 7591. Unknown metadata is ignored rather than refused (\xA73.1), and the answer states what was actually granted rather than what was asked for (\xA73.2.1)."
4614
+ });
4721
4615
  var dynamicClientRegistrationResponse = import_zod17.z.object({
4722
4616
  client_id: import_zod17.z.string().min(1).max(200).meta({
4723
4617
  description: "The identifier this client sends at the authorize and token endpoints. Opaque, and not the app identifier."
@@ -4729,7 +4623,7 @@ var dynamicClientRegistrationResponse = import_zod17.z.object({
4729
4623
  description: "The redirect URIs this registration was accepted for. A code is returned to one of these and nowhere else."
4730
4624
  }),
4731
4625
  grant_types: import_zod17.z.array(import_zod17.z.string()).meta({
4732
- description: "The grants this client may use: `authorization_code` and `refresh_token`."
4626
+ description: 'The grants this client may use. Always exactly `["authorization_code"]` \u2014 a client that asked for `refresh_token` is registered and told here that it did not get one, which is the substitution RFC 7591 \xA73.2.1 permits.'
4733
4627
  }),
4734
4628
  response_types: import_zod17.z.array(import_zod17.z.string()).meta({
4735
4629
  description: "The response types this client may ask for: `code`."
@@ -4744,45 +4638,28 @@ var dynamicClientRegistrationResponse = import_zod17.z.object({
4744
4638
  description: "Always `0`, which is RFC 7591's way of saying the client secret never expires \u2014 there is none. The **registration** itself does expire: a self-registered client that never completes a flow is an unauthenticated write somebody left behind."
4745
4639
  })
4746
4640
  });
4747
- var oauthTokenRequest = import_zod17.z.discriminatedUnion("grant_type", [
4748
- import_zod17.z.object({
4749
- grant_type: import_zod17.z.literal("authorization_code").meta({
4750
- description: "This request exchanges the code from the authorize redirect for tokens."
4751
- }),
4752
- code: import_zod17.z.string().min(1).max(500).meta({
4753
- description: "The authorization code from the redirect. It may be exchanged once."
4754
- }),
4755
- redirect_uri: redirectUri.meta({
4756
- description: "The same redirect URI the authorize request used. It is compared, not merely recorded."
4757
- }),
4758
- client_id: import_zod17.z.string().min(1).max(200).meta({
4759
- description: "The client making the exchange, as registered."
4760
- }),
4761
- code_verifier: import_zod17.z.string().regex(/^[A-Za-z0-9\-._~]{43,128}$/, "code_verifier must be 43-128 unreserved characters (RFC 7636 \xA74.1)").meta({
4762
- description: "The PKCE verifier whose `S256` hash was sent as the challenge at the authorize step. Between `43` and `128` unreserved characters, per RFC 7636 \xA74.1 \u2014 it is compared rather than parsed, so a length nobody checks is a length an attacker chooses. PKCE is mandatory for every client under OAuth 2.1."
4763
- }),
4764
- resource: import_zod17.z.url().optional().meta({
4765
- description: "The resource the token is being requested for, per RFC 8707. It becomes the token's audience, and a resource refuses a token whose audience names something else."
4766
- })
4641
+ var oauthTokenRequest = import_zod17.z.object({
4642
+ grant_type: import_zod17.z.literal("authorization_code").meta({
4643
+ description: "Always `authorization_code`: this request exchanges the code from the authorize redirect for tokens. Any other value \u2014 `refresh_token` included \u2014 is `unsupported_grant_type`, refused before the code is looked up."
4767
4644
  }),
4768
- import_zod17.z.object({
4769
- grant_type: import_zod17.z.literal("refresh_token").meta({
4770
- description: "This request trades a refresh token for a fresh access token."
4771
- }),
4772
- refresh_token: import_zod17.z.string().min(1).max(500).meta({
4773
- description: "The refresh token to spend. Refresh tokens rotate, and presenting one twice is treated as theft rather than as a retry."
4774
- }),
4775
- client_id: import_zod17.z.string().min(1).max(200).meta({
4776
- description: "The client refreshing, as registered."
4777
- }),
4778
- resource: import_zod17.z.url().optional().meta({
4779
- description: "The resource the successor token should be bound to, per RFC 8707. **Carry it forward**: a refresh that drops it mints a token with no audience, and the resource then refuses it one token lifetime after a login that worked, to somebody who did nothing wrong."
4780
- }),
4781
- scope: import_zod17.z.string().max(500).optional().meta({
4782
- description: "A narrower scope for the successor token. RFC 6749 \xA76 lets a refresh narrow scope, never widen it."
4783
- })
4645
+ code: import_zod17.z.string().min(1).max(500).meta({
4646
+ description: "The authorization code from the redirect. It may be exchanged once; a second presentation is `invalid_grant`, the same answer a fabricated code gets."
4647
+ }),
4648
+ redirect_uri: redirectUri.meta({
4649
+ description: "The same redirect URI the authorize request used. It is compared, not merely recorded."
4650
+ }),
4651
+ client_id: import_zod17.z.string().min(1).max(200).meta({
4652
+ description: "The client making the exchange, as registered."
4653
+ }),
4654
+ code_verifier: import_zod17.z.string().regex(/^[A-Za-z0-9\-._~]{43,128}$/, "code_verifier must be 43-128 unreserved characters (RFC 7636 \xA74.1)").meta({
4655
+ description: "The PKCE verifier whose `S256` hash was sent as the challenge at the authorize step. Between `43` and `128` unreserved characters, per RFC 7636 \xA74.1 \u2014 it is compared rather than parsed, so a length nobody checks is a length an attacker chooses. PKCE is mandatory for every client under OAuth 2.1."
4656
+ }),
4657
+ resource: import_zod17.z.url().optional().meta({
4658
+ description: "The resource the token is being requested for, per RFC 8707. It must match the audience the code was authorized for, or the answer is `invalid_target`; omitted, the code's own audience stands. It becomes the token's `aud`, and a resource refuses a token whose audience names something else \u2014 which is what keeps a token minted for one app out of another app's endpoint."
4784
4659
  })
4785
- ]);
4660
+ }).meta({
4661
+ description: "RFC 6749 \xA74.1.3's authorization-code exchange with PKCE, as either MCP authorization server reads it. Sent as `application/x-www-form-urlencoded`, per \xA74.1.3, though the server accepts a JSON body too."
4662
+ });
4786
4663
  var oauthTokenResponse = import_zod17.z.object({
4787
4664
  access_token: import_zod17.z.string().min(1).meta({
4788
4665
  description: "The bearer token. It is the same token the client login mints \u2014 only the envelope differs, because an RFC-compliant client parses this one and knows nothing about Fleetless."
@@ -4867,20 +4744,18 @@ var oauthAuthorizeQuery = import_zod17.z.object({
4867
4744
  }),
4868
4745
  resource: import_zod17.z.string().optional().meta({
4869
4746
  description: "RFC 8707 resource indicator: the API origin or the MCP endpoint the token is for. Checked against the resources this server issues tokens for **on behalf of this client's app**; a mismatch is `invalid_target` on the callback."
4870
- }),
4871
- scope: import_zod17.z.string().optional().meta({
4872
- description: "Space-separated scopes. Carried onto the interaction and read again at consent \u2014 **nothing is enforced at this step**, so an unknown scope is not a refusal here."
4873
4747
  })
4748
+ // **No `scope`, because this authorization server issues none.** The field
4749
+ // was here describing itself as "carried onto the interaction and read
4750
+ // again at consent"; neither authorize handler reads it, the interaction
4751
+ // row has no column for it, and the consent screen answers `scopes: []`.
4752
+ // A parameter documented as carried and in fact dropped is worse than one
4753
+ // that is absent.
4874
4754
  }).meta({
4875
- description: "The authorization request an MCP client sends. The two parameters above `response_type` are validated first and refuse flat; every parameter after them reports to the callback."
4755
+ description: "The authorization request an MCP client sends, per RFC 6749 \xA74.1.1 with mandatory PKCE. The handler reads it parameter by parameter rather than through one parse, because the answers differ: `client_id` and `redirect_uri` are refused flat, with no redirect, since until both are confirmed there is no trusted target to bounce a browser to, and everything after them is reported to the client's own callback as query parameters."
4876
4756
  });
4877
- var oauthRegisterQuery = import_zod17.z.object({
4878
- app_identifier: import_zod17.z.string().min(1).meta({
4879
- description: "The app the dynamic client registers under, as `appIdentifier` spells it. Required: missing and unknown answer the same `400 invalid_request` in the `oauthError` dialect. Whether that app has MCP enabled at all is a separate, later refusal (`access_denied`)."
4880
- })
4881
- }).meta({ description: "The app a dynamic client registers under \u2014 the one parameter RFC 7591 has no body field for." });
4882
4757
 
4883
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/routes.js
4758
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/routes.js
4884
4759
  var MCP_APP = MCP_APP_PATHS(":appIdentifier");
4885
4760
  var APP_IDENTIFIER = {
4886
4761
  name: "appIdentifier",
@@ -5443,7 +5318,7 @@ var ROUTES = [
5443
5318
  response: appUser,
5444
5319
  errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found", "email_taken", "target_state_conflict", "quota_exceeded"],
5445
5320
  transport: "http",
5446
- notes: "The developer-authenticated door into the app's user table, and the one place `409 email_taken` is an honest answer about an app user: the caller is authenticated into this app already, so telling them the address is taken discloses nothing they could not read from the listing beside it. `POST /api/client/register` answers `202` to the same fact, because there the caller is a stranger. `404 not_found` is the app, or a `role_id` that is not a role of it \u2014 a role of another app is refused rather than stored, since a user holding one would carry rights nothing in this app can resolve. **The password policy answers `400 validation_error`**, not a code of its own: the twelve-character minimum is the `password` field's schema rule, and every route in this repository that takes a password refuses a short one exactly the way it refuses any other malformed field. An account created here is `active` immediately: a developer entering somebody by hand has made the decision the verification mail automates, and its address counts as proven. `409 target_state_conflict` names `default_role_id` when `role_id` is absent and the app has no default role, or its default names a role that no longer resolves \u2014 a user with no role holds rights nothing in this app can read, so nothing is created. \n\n**`409 quota_exceeded` when the org holds as many app users as `max_end_users` allows**, counted across every app of the org \u2014 the same number `GET /api/org/quotas` reports as `usage.max_end_users`, since the same address in two apps is two accounts. `details` carries `{ quota, limit }`, as every count quota's refusal does. The check is at **creation** only: an existing user signs in, is patched and is deleted at the quota exactly as under it, because a protection limit that also froze the accounts already made would be an outage rather than a limit."
5321
+ notes: "The developer-authenticated door into the app's user table, and the one place `409 email_taken` is an honest answer about an app user: the caller is authenticated into this app already, so telling them the address is taken discloses nothing they could not read from the listing beside it. `POST /api/client/register` answers `202` to the same fact, because there the caller is a stranger. `404 not_found` is the app, or a `role_id` that is not a role of it \u2014 a role of another app is refused rather than stored, since a user holding one would carry rights nothing in this app can resolve. **The password policy answers `400 validation_error`**, not a code of its own: the twelve-character minimum is the `password` field's schema rule, and every route that takes a password refuses a short one exactly the way it refuses any other malformed field. An account created here is `active` immediately: a developer entering somebody by hand has made the decision the verification mail automates, and its address counts as proven. `409 target_state_conflict` names `default_role_id` when `role_id` is absent and the app has no default role, or its default names a role that no longer resolves \u2014 a user with no role holds rights nothing in this app can read, so nothing is created. \n\n**`409 quota_exceeded` when the org holds as many app users as `max_end_users` allows**, counted across every app of the org \u2014 the same number `GET /api/org/quotas` reports as `usage.max_end_users`, since the same address in two apps is two accounts. `details` carries `{ quota, limit }`, as every count quota's refusal does. The check is at **creation** only: an existing user signs in, is patched and is deleted at the quota exactly as under it, because a protection limit that also froze the accounts already made would be an outage rather than a limit."
5447
5322
  },
5448
5323
  {
5449
5324
  method: "GET",
@@ -5556,7 +5431,7 @@ var ROUTES = [
5556
5431
  response: null,
5557
5432
  errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found"],
5558
5433
  transport: "http",
5559
- notes: "The support door beside `DELETE /api/client/mcp/grants/:clientId`, which is the same act by the person themselves. Audited as `app_user.mcp_grant_revoked`, whose `details` carry the client id and the app's uuid \u2014 and nothing else, in particular no token and no name the client chose for itself. \n\n**`204` whether or not there was anything to withdraw**, so a double-clicked button and a client id no grant names both land on the end state the caller asked for. The alternative \u2014 `404` for a client this user never approved \u2014 would make the route an oracle for which clients somebody has connected, answered before the listing beside it was read; and it would turn the ordinary retry into a refusal. Only a withdrawal that actually ended a standing agreement writes an audit event, so the log counts consents ended rather than buttons pressed. **`404` is still the app and the user**, which are the two things the caller must own. \n\n**It does not end an MCP session already running.** The access token that consent produced is a fifteen-minute bearer the transport checks against the account, not against this table, so a session in flight survives until it expires; there is no refresh grant on this authorization server, so nothing can extend it, and the next authorization shows the consent screen again. Blocking the account (`PATCH /api/apps/:id/users/:userId`) is what ends a live session now, and it ends every one of their sessions rather than this client's."
5434
+ notes: "The support door beside `DELETE /api/client/mcp/grants/:clientId`, which is the same act by the person themselves. Audited as `app_user.mcp_grant_revoked`, whose `details` carry the client id and the app's uuid \u2014 and nothing else, in particular no token and no name the client chose for itself. \n\n**`204` whether or not there was anything to withdraw**, so a double-clicked button and a client id no grant names both land on the end state the caller asked for. The alternative \u2014 `404` for a client this user never approved \u2014 would make the route an oracle for which clients somebody has connected, answered before the listing beside it was read; and it would turn the ordinary retry into a refusal. Only a withdrawal that actually ended a standing agreement writes an audit event, so the log counts consents ended rather than buttons pressed. **`404` is still the app and the user**, which are the two things the caller must own. \n\n**It ends a session already running, at that client's very next call.** The app's MCP endpoint reads this table on every request, beside the account checks it already makes, so a withdrawn client is answered `401` with the `WWW-Authenticate` challenge that sends it back to the consent screen. The refusal is keyed on the `client_id` the access token carries, so it bites at the next call rather than at the next token: that token is still unexpired \u2014 up to fifteen minutes are left on it \u2014 and is refused anyway. Only this client stops. The person's other clients and their own use of the app are untouched, which is the difference from blocking the account (`PATCH /api/apps/:id/users/:userId`)."
5560
5435
  },
5561
5436
  {
5562
5437
  method: "GET",
@@ -5630,7 +5505,7 @@ var ROUTES = [
5630
5505
  transport: "http",
5631
5506
  notes: "An invitation that was already accepted is not pending and answers `404`, the same answer one that never existed gets \u2014 the account it created is a user now, and deleting that is `DELETE /api/apps/:id/users/:userId`. A revoked token answers `410 token_spent` at `POST /api/client/invitations/accept`, the same answer one that expired or never existed gets \u2014 the developer withdrew it deliberately, and an answer saying so would tell whoever still holds the link that it was once real."
5632
5507
  },
5633
- /* --------------------------------------- the app's OIDC providers (D4) */
5508
+ /* ------------------------------------------- the app's OIDC providers */
5634
5509
  {
5635
5510
  method: "GET",
5636
5511
  path: "/api/apps/:id/oidc-providers",
@@ -6131,11 +6006,11 @@ var ROUTES = [
6131
6006
  status: 201,
6132
6007
  params: [],
6133
6008
  query: null,
6134
- request: null,
6009
+ request: dynamicClientRegistrationRequest,
6135
6010
  response: dynamicClientRegistrationResponse,
6136
6011
  errors: ["rate_limited"],
6137
6012
  transport: "http",
6138
- notes: "RFC 7591, and deliberately **not** parsed against `dynamicClientRegistrationRequest`: that shape is strict, and a strict schema here would answer `400` to a conforming client and take the whole paste-the-URL flow down with it. `client_name` and `redirect_uris` are read by hand; everything else is ignored. What comes back is what was actually granted, which \xA73.2.1 allows a server to substitute \u2014 this authorization server issues `authorization_code` only, so a client that asked for `refresh_token` is registered and told plainly that it did not get one. The registration carries a TTL. Refusals are `oauthError`; the rate limiter answers `apiError`."
6013
+ notes: "RFC 7591. **The request schema is what this endpoint accepts, not what it parses**: the handler reads the body field by field, because \xA73.2.2 distinguishes `invalid_redirect_uri` from `invalid_client_metadata` and one `safeParse` failure cannot say which of the two a caller earned. The shape is deliberately **not** strict, which is the schema agreeing with \xA73.1 rather than a gap in it \u2014 a conforming client sends `client_uri`, `logo_uri` and `software_id`, and both the schema and the server ignore them. `client_name` and `redirect_uris` are the two fields read; `grant_types`, `response_types` and `scope` are accepted and ignored. What comes back is what was actually granted, which \xA73.2.1 allows a server to substitute \u2014 this authorization server issues `authorization_code` only, so a client that asked for `refresh_token` is registered and told plainly that it did not get one. The registration carries a TTL. Refusals are `oauthError`; the rate limiter answers `apiError`."
6139
6014
  },
6140
6015
  {
6141
6016
  method: "GET",
@@ -6148,12 +6023,12 @@ var ROUTES = [
6148
6023
  ownerTier: false,
6149
6024
  status: 302,
6150
6025
  params: [],
6151
- query: null,
6026
+ query: oauthAuthorizeQuery,
6152
6027
  request: null,
6153
6028
  response: null,
6154
6029
  errors: [],
6155
6030
  transport: "http",
6156
- notes: "Client and `redirect_uri` are validated first and a failure there never redirects, the same open-redirect discipline the app flow applies; those refusals are `oauthError`. Exact `redirect_uri` matching for both client kinds \u2014 the loopback-port wildcard of RFC 8252 \xA77.3 belongs to the one central client alone, whose URIs are configured ahead of time and cannot name an ephemeral port. A client that registered itself seconds ago can name the port it bound, and widening the wildcard there would only widen where a stolen `client_id` may send a browser. Nothing about the person is decided here \u2014 the next card asks for an email address and the password step after it resolves the account; this route knows only the client."
6031
+ notes: "**The query schema is what this endpoint accepts, not what it parses**: the handler reads it parameter by parameter because the answers differ, and one parse would collapse them. Client and `redirect_uri` are validated first and a failure there never redirects, the same open-redirect discipline the app flow applies; those refusals are `oauthError`. Exact `redirect_uri` matching for both client kinds \u2014 the loopback-port wildcard of RFC 8252 \xA77.3 belongs to the one central client alone, whose URIs are configured ahead of time and cannot name an ephemeral port. A client that registered itself seconds ago can name the port it bound, and widening the wildcard there would only widen where a stolen `client_id` may send a browser. Nothing about the person is decided here \u2014 the next card asks for an email address and the password step after it resolves the account; this route knows only the client."
6157
6032
  },
6158
6033
  {
6159
6034
  method: "GET",
@@ -6189,7 +6064,7 @@ var ROUTES = [
6189
6064
  response: null,
6190
6065
  errors: ["rate_limited", "validation_error", "token_spent"],
6191
6066
  transport: "http",
6192
- notes: 'The identifier-first step, with nothing left to identify: Fleetless users are password-only (design D1/D7), so **this step does not read the address at all** \u2014 it renders the password card for a known address, an unknown one and an empty one alike, and the login step below answers the same `401` for all three. That is a property of the shape rather than of two branches agreeing: there is no lookup here whose result could differ. A browser form post gets the password card; a JSON caller gets `{ "next" }`, which has no schema. Still rate limited per (route, ip, email), because it is an unauthenticated endpoint that renders a page.'
6067
+ notes: 'The identifier-first step, with nothing left to identify: Fleetless users are password-only, so **this step does not read the address at all** \u2014 it renders the password card for a known address, an unknown one and an empty one alike, and the login step below answers the same `401` for all three. That is a property of the shape rather than of two branches agreeing: there is no lookup here whose result could differ. A browser form post gets the password card; a JSON caller gets `{ "next" }`, which has no schema. Still rate limited per (route, ip, email), because it is an unauthenticated endpoint that renders a page.'
6193
6068
  },
6194
6069
  {
6195
6070
  method: "POST",
@@ -6257,7 +6132,7 @@ var ROUTES = [
6257
6132
  status: 200,
6258
6133
  params: [],
6259
6134
  query: null,
6260
- request: null,
6135
+ request: oauthTokenRequest,
6261
6136
  response: oauthTokenResponse,
6262
6137
  errors: [],
6263
6138
  transport: "http",
@@ -6443,9 +6318,9 @@ var ROUTES = [
6443
6318
  response: null,
6444
6319
  errors: ["unauthorized", "forbidden"],
6445
6320
  transport: "http",
6446
- notes: "JSON-RPC over MCP's Streamable HTTP, so neither the request nor the response is a shape contracts describes; the tool arguments and results are the schemas in each tool definition. **Fleetless users only** \u2014 an app's users reach their own app endpoint instead. **The bearer is verified inside the handler**, not by a route guard: the identity comes from the token and the path names none, and the refusal has to carry a `WWW-Authenticate` challenge that a guard shared with the REST surface does not send. `Origin` is checked against the cloud's own, and a foreign one is the `403 forbidden` above. **Both catalogs, unconditionally**: every caller admitted here is a Fleetless user, so the tool list has nothing left to vary with and the admin-ness check on `tools/call` is gone \u2014 a console tool that is still narrower than the catalog refuses for itself (`console_robot_delete` answers `tier_required` to a non-Owner). `403 forbidden` is also what an `mcp_session` token whose subject is an **app user** gets: this endpoint serves the team only, and such a token belongs to its own app's endpoint. The code is `forbidden` rather than `mcp_disabled` because nothing is switched off \u2014 the caller is at the wrong server \u2014 and per-app sign-in mints exactly such tokens, so the two states must not share a word. `mcp_access_denied` is gone with the per-user override and the group flag it read: every Fleetless user has MCP access here (D1). Stateless: a fresh transport per request, no session id, nothing survives the call."
6321
+ notes: "JSON-RPC over MCP's Streamable HTTP, so neither the request nor the response is a shape contracts describes; the tool arguments and results are the schemas in each tool definition. **Fleetless users only** \u2014 an app's users reach their own app endpoint instead. **The bearer is verified inside the handler**, not by a route guard: the identity comes from the token and the path names none, and the refusal has to carry a `WWW-Authenticate` challenge that a guard shared with the REST surface does not send. `Origin` is checked against the cloud's own, and a foreign one is the `403 forbidden` above. **Both catalogs, unconditionally**: every caller admitted here is a Fleetless user, so the tool list has nothing left to vary with and the admin-ness check on `tools/call` is gone \u2014 a console tool that is still narrower than the catalog refuses for itself (`console_robot_delete` answers `tier_required` to a non-Owner). `403 forbidden` is also what an `mcp_session` token whose subject is an **app user** gets: this endpoint serves the team only, and such a token belongs to its own app's endpoint. The code is `forbidden` rather than `mcp_disabled` because nothing is switched off \u2014 the caller is at the wrong server \u2014 and per-app sign-in mints exactly such tokens, so the two states must not share a word. `mcp_access_denied` is gone with the per-user override and the group flag it read: every Fleetless user has MCP access here. Stateless: a fresh transport per request, no session id, nothing survives the call."
6447
6322
  },
6448
- /* ----------------------------------------- mcp (one app's own server, D7) */
6323
+ /* --------------------------------------------- mcp (one app's own server) */
6449
6324
  {
6450
6325
  method: "POST",
6451
6326
  path: MCP_APP.endpoint,
@@ -6462,7 +6337,7 @@ var ROUTES = [
6462
6337
  response: null,
6463
6338
  errors: ["not_found", "unauthorized", "forbidden"],
6464
6339
  transport: "http",
6465
- notes: "JSON-RPC over MCP's Streamable HTTP, so neither the request nor the response is a shape contracts describes \u2014 exactly as `POST /mcp` is, and stateless for the same reason: a fresh transport per request, no session id, nothing surviving the call. **App users only.** The tools are this app's robots filtered by the caller's role, built by the same builder `GET /api/apps/:id/roles/:roleId/mcp-tools` previews, so the console's preview and the live catalog cannot drift. The console tool family belongs to the central endpoint and is offered here to nobody. \n\n**The bearer is verified inside the handler**, not by a route guard, for the two reasons the central endpoint gives \u2014 the identity comes from the token and the path names none of it, and the refusal has to carry a `WWW-Authenticate` challenge a guard shared with the REST surface does not send. The challenge names **this app's** protected-resource document (RFC 9728's `resource_metadata`), which is how an MCP client discovers the right authorization server from a bare `401`; pointing it at the central document would send every app's client to the wrong sign-in. \n\n**`404 not_found` covers an identifier no app carries AND an app whose `appAuthConfig.mcp_enabled` is off \u2014 one answer for both, the same one the two metadata documents and `register` give.** A separate `403 mcp_disabled` here would hand an anonymous caller a three-way oracle (`404` = no such app, `403` = the app exists with MCP off, `401` = the app exists and is live), which is exactly the distinction discovery collapses; there is no point collapsing it in one place and publishing it in another. The switch is re-read on every request rather than cached off the token, so a developer turning it off ends the sessions already running, and it is decided **before the bearer is looked at** \u2014 the reverse of the usual order, and deliberate: it is a fact about the path, an app identifier is public, and an absent server that answered `401` would send a client hunting a credential no credential can satisfy. `401 unauthorized` is a missing, unverifiable or expired bearer, or an `aud` that is not this endpoint. `403 forbidden` is a token that verifies and is not this app's user: another app's session, a Fleetless user's central `mcp_session`, an account that is `blocked` or still `pending_verification`, or a foreign `Origin`."
6340
+ notes: "JSON-RPC over MCP's Streamable HTTP, so neither the request nor the response is a shape contracts describes \u2014 exactly as `POST /mcp` is, and stateless for the same reason: a fresh transport per request, no session id, nothing surviving the call. **App users only.** The tools are this app's robots filtered by the caller's role, built by the same builder `GET /api/apps/:id/roles/:roleId/mcp-tools` previews, so the console's preview and the live catalog cannot drift. The console tool family belongs to the central endpoint and is offered here to nobody. \n\n**The bearer is verified inside the handler**, not by a route guard, for the two reasons the central endpoint gives \u2014 the identity comes from the token and the path names none of it, and the refusal has to carry a `WWW-Authenticate` challenge a guard shared with the REST surface does not send. The challenge names **this app's** protected-resource document (RFC 9728's `resource_metadata`), which is how an MCP client discovers the right authorization server from a bare `401`; pointing it at the central document would send every app's client to the wrong sign-in. \n\n**`404 not_found` covers an identifier no app carries AND an app whose `appAuthConfig.mcp_enabled` is off \u2014 one answer for both, the same one the two metadata documents and `register` give.** A separate `403 mcp_disabled` here would hand an anonymous caller a three-way oracle (`404` = no such app, `403` = the app exists with MCP off, `401` = the app exists and is live), which is exactly the distinction discovery collapses; there is no point collapsing it in one place and publishing it in another. The switch is re-read on every request rather than cached off the token, so a developer turning it off ends the sessions already running, and it is decided **before the bearer is looked at** \u2014 the reverse of the usual order, and deliberate: it is a fact about the path, an app identifier is public, and an absent server that answered `401` would send a client hunting a credential no credential can satisfy. `401 unauthorized` is a missing, unverifiable or expired bearer, an `aud` that is not this endpoint, or **a consent this person has since withdrawn from the client the token was minted for**: the access token names its client, and the standing consent is re-read here on every request exactly as the account is, so `DELETE /api/client/mcp/grants/:clientId` and its developer twin bite at the next call rather than when the token expires. `403 forbidden` is a token that verifies and is not this app's user: another app's session, a Fleetless user's central `mcp_session`, an account that is `blocked` or still `pending_verification`, or a foreign `Origin`."
6466
6341
  },
6467
6342
  {
6468
6343
  method: "GET",
@@ -6480,7 +6355,7 @@ var ROUTES = [
6480
6355
  response: null,
6481
6356
  errors: ["not_found", "unauthorized", "forbidden"],
6482
6357
  transport: "http",
6483
- notes: "MCP's Streamable HTTP gives this path three verbs: `POST` carries JSON-RPC, `GET` opens the server-initiated SSE stream, and `DELETE` ends a session. This server has no sessions \u2014 the argument is in `MCP_PROTOCOL_VERSION`'s own note, and W8's second cloud instance is where a per-process session map would break \u2014 so `GET` and `DELETE` answer `405`, which is what a client is built to fall back from. \n\n**The `405` is this cloud's own answer, not the SDK's**, and the difference was measured: MCP SDK 1.30.0 opens an SSE stream on `GET` (`handleGetRequest`) and answers `200` on `DELETE` (`handleDeleteRequest`), neither of which a stateless server has any business doing, so the cloud writes the `405` itself in the transport's own JSON-RPC error shape with `Allow: POST`. \n\n**The row exists so that the `405` is not a `404`.** An unregistered verb answers `404`, and at a path whose last segment is an app identifier a `404` already means *no such app* \u2014 one answer for two states, which is the failure this project keeps paying for. Registering the verb lets the endpoint say \"this app's server is here; this verb is not part of it\". The central `/mcp` registers neither verb and does not need to: its path takes no parameter, so nothing can misread its `404`. \n\n**The `405` body is the transport's JSON-RPC error object, not the `apiError` envelope.** The three codes above are the refusals that come *first* \u2014 the app, its switch, then the bearer, in the order `POST` describes \u2014 and they are `apiError` because they are answered before the transport is reached at all. If this server ever becomes stateful, this row and the `DELETE` beside it are where that lands, and the cloud's route-manifest test is what would make both repositories notice."
6358
+ notes: "MCP's Streamable HTTP gives this path three verbs: `POST` carries JSON-RPC, `GET` opens the server-initiated SSE stream, and `DELETE` ends a session. This server has no sessions \u2014 the argument is in `MCP_PROTOCOL_VERSION`'s own note, and a per-process session map is what breaks at the second cloud instance \u2014 so `GET` and `DELETE` answer `405`, which is what a client is built to fall back from. \n\n**The `405` is this cloud's own answer, not the SDK's**, and the two differ: MCP SDK 1.30.0 opens an SSE stream on `GET` (`handleGetRequest`) and answers `200` on `DELETE` (`handleDeleteRequest`), neither of which a stateless server has any business doing, so the cloud writes the `405` itself in the transport's own JSON-RPC error shape with `Allow: POST`. \n\n**The row exists so that the `405` is not a `404`.** An unregistered verb answers `404`, and at a path whose last segment is an app identifier a `404` already means *no such app* \u2014 so an unregistered verb and an unknown app would answer identically. Registering the verb lets the endpoint say \"this app's server is here; this verb is not part of it\". The central `/mcp` registers neither verb and does not need to: its path takes no parameter, so nothing can misread its `404`. \n\n**The `405` body is the transport's JSON-RPC error object, not the `apiError` envelope.** The three codes above are the refusals that come *first* \u2014 the app, its switch, then the bearer, in the order `POST` describes \u2014 and they are `apiError` because they are answered before the transport is reached at all. If this server ever becomes stateful, this row and the `DELETE` beside it are where that lands, and the cloud's route-manifest test is what would make both repositories notice."
6484
6359
  },
6485
6360
  {
6486
6361
  method: "DELETE",
@@ -6534,7 +6409,7 @@ var ROUTES = [
6534
6409
  response: authorizationServerMetadata,
6535
6410
  errors: ["not_found"],
6536
6411
  transport: "http",
6537
- notes: "RFC 8414, for the issuer `<PUBLIC_API_BASE_URL>/mcp/<identifier>` \u2014 the same path rule as the document above, and the same `404` for a switched-off app. `registration_endpoint` is present for the reason the central document states: a client that finds it registers itself and never asks a person for a `client_id`. \n\n**`issuer`, `token_endpoint` and the resource identifier are minted from the canonical public base, never from the friendly `mcp.fleetless.dev` alias or the request's `Host`**, because a client checks a minted token's `iss` and `aud` against these exact strings. \n\n**Unlike the central document, `authorization_endpoint` does not move to an auth-portal origin**, and there is nothing here for one to serve: this authorization step renders no Fleetless page at all. It redirects to the app's own `mcp_login_url` (D7), which is on the developer's origin already."
6412
+ notes: "RFC 8414, for the issuer `<PUBLIC_API_BASE_URL>/mcp/<identifier>` \u2014 the same path rule as the document above, and the same `404` for a switched-off app. `registration_endpoint` is present for the reason the central document states: a client that finds it registers itself and never asks a person for a `client_id`. \n\n**`issuer`, `token_endpoint` and the resource identifier are minted from the canonical public base, never from the friendly `mcp.fleetless.dev` alias or the request's `Host`**, because a client checks a minted token's `iss` and `aud` against these exact strings. \n\n**Unlike the central document, `authorization_endpoint` does not move to an auth-portal origin**, and there is nothing here for one to serve: this authorization step renders no Fleetless page at all. It redirects to the app's own `mcp_login_url`, which is on the developer's origin already."
6538
6413
  },
6539
6414
  {
6540
6415
  method: "POST",
@@ -6548,11 +6423,11 @@ var ROUTES = [
6548
6423
  status: 201,
6549
6424
  params: [APP_IDENTIFIER],
6550
6425
  query: null,
6551
- request: null,
6426
+ request: dynamicClientRegistrationRequest,
6552
6427
  response: dynamicClientRegistrationResponse,
6553
6428
  errors: ["rate_limited", "not_found"],
6554
6429
  transport: "http",
6555
- notes: "RFC 7591, and deliberately **not** parsed against `dynamicClientRegistrationRequest`, for the reason `POST /mcp/oauth/register` gives: that shape is strict, and a strict schema here would answer `400` to a conforming client and take the whole paste-the-URL flow down with it. `client_name` and `redirect_uris` are read by hand; everything else is ignored, and what comes back is what was actually granted, which \xA73.2.1 allows \u2014 `authorization_code` only, so a client that asked for `refresh_token` is registered and told plainly that it did not get one. The registration carries a TTL. \n\n**The registration is scoped to this app.** A `client_id` minted here authorizes at this app's endpoint and nowhere else, so a client registered against one app cannot walk into another's authorize with it, and a developer who switches MCP off is not left with strangers' registrations valid somewhere adjacent. \n\nRefusals are `oauthError`; the rate limiter and `404 not_found` answer `apiError`. That `404` covers an unknown identifier **and** an app with the switch off, mirroring the two metadata documents this endpoint is discovered from \u2014 a client that could not read those has no business registering here, and giving it a third distinct answer would only tell it something the documents deliberately do not."
6430
+ notes: "RFC 7591, the same wire and the same handler as `POST /mcp/oauth/register` \u2014 one implementation, because a second answer to \"is this redirect URI acceptable\" would agree with the first only by luck. The request schema is what the endpoint accepts rather than what it parses, for the reason that row gives: \xA73.2.2 needs two distinguishable refusals and one `safeParse` failure offers one. `client_name` and `redirect_uris` are read; `grant_types`, `response_types` and `scope` are accepted and ignored, and what comes back is what was actually granted, which \xA73.2.1 allows \u2014 `authorization_code` only, so a client that asked for `refresh_token` is registered and told plainly that it did not get one. The registration carries a TTL. \n\n**The registration is scoped to this app.** A `client_id` minted here authorizes at this app's endpoint and nowhere else, so a client registered against one app cannot walk into another's authorize with it, and a developer who switches MCP off is not left with strangers' registrations valid somewhere adjacent. \n\nRefusals are `oauthError`; the rate limiter and `404 not_found` answer `apiError`. That `404` covers an unknown identifier **and** an app with the switch off, mirroring the two metadata documents this endpoint is discovered from \u2014 a client that could not read those has no business registering here, and giving it a third distinct answer would only tell it something the documents deliberately do not."
6556
6431
  },
6557
6432
  {
6558
6433
  method: "GET",
@@ -6565,12 +6440,12 @@ var ROUTES = [
6565
6440
  ownerTier: false,
6566
6441
  status: 302,
6567
6442
  params: [APP_IDENTIFIER],
6568
- query: null,
6443
+ query: oauthAuthorizeQuery,
6569
6444
  request: null,
6570
6445
  response: null,
6571
6446
  errors: ["not_found", "target_state_conflict"],
6572
6447
  transport: "http",
6573
- notes: '**Fleetless renders no page here, and that is the whole of D7.** The route writes an interaction \u2014 ten minutes, as the OIDC ones live \u2014 and redirects to `appAuthConfig.mcp_login_url` with `{interaction}` filled in. The app then authenticates the person with its own UI, reads `GET /api/client/mcp/interactions/:id` to show the client\'s claimed name and the scopes it asked for, and calls approve or deny. \n\nClient and `redirect_uri` are validated first and a failure there never redirects \u2014 the open-redirect discipline `GET /mcp/oauth/authorize` and `GET /api/client/oidc/:slug/start` both keep \u2014 and those refusals are RFC 6749\'s flat `oauthError`, which is why none of them appear above. `redirect_uri` is matched **exactly** against the registration, with no loopback-port wildcard: every client here registered itself minutes ago and can name the port it bound, so a wildcard would only widen where a stolen `client_id` may send a browser. \n\nThe two codes above are the `apiError` envelope because they are refusals about the **app**, decided before an OAuth parameter is looked at. **`404 not_found` covers an identifier no app carries AND an app with MCP switched off** \u2014 the same single answer the two metadata documents, `register` and the transport give. An earlier draft answered `403 mcp_disabled` here, on the argument that a client which registered while the switch was on is owed the difference between "turned off" and "mistyped"; that argument does not survive the caller being anonymous. This route takes no credential, so the extra code was readable by anyone who could type an identifier, and it handed back precisely the existence distinction every neighbouring route collapses. `mcp_disabled` survives only where the caller has already proved they belong to the app \u2014 the two decision routes under `/api/client/mcp/interactions/:id`. `409 target_state_conflict` names `mcp_login_url` with rule `not_set`: MCP is enabled and no page is configured to send the person to. It is the same code and the same shape `send_mail` answers for an unconfigured `invite_url`, and the refusal is the honest one \u2014 Fleetless has nowhere to redirect, and rendering a page of its own instead would contradict D2.'
6448
+ notes: 'The same query as `GET /mcp/oauth/authorize`, read the same way \u2014 parameter by parameter, because the answers differ and one parse would collapse them. \n\n**Fleetless renders no page here**, and that is the whole of it. The route writes an interaction \u2014 ten minutes, as the OIDC ones live \u2014 and redirects to `appAuthConfig.mcp_login_url` with `{interaction}` filled in. The app then authenticates the person with its own UI, reads `GET /api/client/mcp/interactions/:id` to show the client\'s claimed name and the scopes it asked for, and calls approve or deny. \n\nClient and `redirect_uri` are validated first and a failure there never redirects \u2014 the open-redirect discipline `GET /mcp/oauth/authorize` and `GET /api/client/oidc/:slug/start` both keep \u2014 and those refusals are RFC 6749\'s flat `oauthError`, which is why none of them appear above. `redirect_uri` is matched **exactly** against the registration, with no loopback-port wildcard: every client here registered itself minutes ago and can name the port it bound, so a wildcard would only widen where a stolen `client_id` may send a browser. \n\nThe two codes above are the `apiError` envelope because they are refusals about the **app**, decided before an OAuth parameter is looked at. **`404 not_found` covers an identifier no app carries AND an app with MCP switched off** \u2014 the same single answer the two metadata documents, `register` and the transport give. An earlier draft answered `403 mcp_disabled` here, on the argument that a client which registered while the switch was on is owed the difference between "turned off" and "mistyped"; that argument does not survive the caller being anonymous. This route takes no credential, so the extra code was readable by anyone who could type an identifier, and it handed back precisely the existence distinction every neighbouring route collapses. `mcp_disabled` survives only where the caller has already proved they belong to the app \u2014 the two decision routes under `/api/client/mcp/interactions/:id`. `409 target_state_conflict` names `mcp_login_url` with rule `not_set`: MCP is enabled and no page is configured to send the person to. It is the same code and the same shape `send_mail` answers for an unconfigured `invite_url`, and the refusal is the honest one \u2014 Fleetless has nowhere to redirect, and rendering a page of its own would contradict the rule that Fleetless shows an app user no page.'
6574
6449
  },
6575
6450
  {
6576
6451
  method: "POST",
@@ -6584,7 +6459,7 @@ var ROUTES = [
6584
6459
  status: 200,
6585
6460
  params: [APP_IDENTIFIER],
6586
6461
  query: null,
6587
- request: null,
6462
+ request: oauthTokenRequest,
6588
6463
  response: oauthTokenResponse,
6589
6464
  errors: [],
6590
6465
  transport: "http",
@@ -6842,7 +6717,7 @@ var ROUTES = [
6842
6717
  response: null,
6843
6718
  errors: ["rate_limited"],
6844
6719
  transport: "http",
6845
- notes: "**One callback URL for every app and every provider**, and the value of `appAuthConfig.oidc_callback_url` \u2014 the string a developer registers at their IdP. `CLIENT_OIDC_CALLBACK_PATH` in `client-auth.ts` is the single spelling of this path; the URL is that path on the cloud's canonical public base, never a friendly alias, because the provider compares the redirect target against the one string it was given. \n\n**Rate limited per ip, generously.** The first draft left this route unlimited on the argument that the caller is an identity provider redirecting somebody's browser, so a limiter would drop real sign-ins on the strength of traffic none of those people sent. The cost of that argument is a `state` obtained from one `start` being replayable for the interaction's full ten minutes, unbounded and unauthenticated, with every replay driving a server-side POST to the developer's token endpoint and one audit row into their org \u2014 an amplifier against a third party. The interaction is now spent on **every** terminal outcome, refusals included, which closes the replay itself; the limiter is the ceiling on how fast the attempts may arrive at all. It is per ip and sized for a browser, so a person completing a sign-in never meets it. `429 rate_limited` is the one `apiError` this route can answer, and it is not a sign-in outcome \u2014 it is a refusal to begin the work, which is why it does not ride back to the app as an `?error=`. \n\nThe query is the **provider's** rather than a Fleetless shape, and `clientOidcCallbackQuery` describes it **without being strict**: `state` always, `code` on success, `error` and `error_description` on the provider's own refusal, and whatever else that provider adds \u2014 RFC 9207's `iss`, a `session_state`, a vendor field. Refusing those would refuse conforming providers, the trap `POST /mcp/oauth/register` documents avoiding. `state` is the required field because it is the only one Fleetless minted. \n\n**It lists `rate_limited` and no other code, because every sign-in outcome it has is a redirect.** Success and failure alike are a `302` to the app's own `redirect_uri`: `?code=\u2026&state=\u2026` when a session was resolved, `?error=<clientOidcErrorCode>&state=\u2026` when it was not, so the app renders its own message and can bind either answer to the request it started. Fleetless shows an app user no page (D2). \n\n**The one exception is a `state` that resolves to no interaction** \u2014 unknown, hand-edited, or past its ten minutes. Then there is no confirmed redirect target to carry the answer to, and bouncing a browser to an unvalidated one is the hole the whole flow is arranged to avoid, so the cloud renders an HTML problem page at `400`. That is the only Fleetless-rendered surface an app user can reach. It is HTML rather than an `apiError`, which is why no code is listed: a code here would document an envelope no caller receives, and this manifest's other HTML pages (`GET /mcp/oauth/interaction/:id`, `GET /console/oauth/interaction/:id`) say their status in prose for the same reason."
6720
+ notes: "**One callback URL for every app and every provider**, and the value of `appAuthConfig.oidc_callback_url` \u2014 the string a developer registers at their IdP. `CLIENT_OIDC_CALLBACK_PATH` in `client-auth.ts` is the single spelling of this path; the URL is that path on the cloud's canonical public base, never a friendly alias, because the provider compares the redirect target against the one string it was given. \n\n**Rate limited per ip, generously.** The first draft left this route unlimited on the argument that the caller is an identity provider redirecting somebody's browser, so a limiter would drop real sign-ins on the strength of traffic none of those people sent. The cost of that argument is a `state` obtained from one `start` being replayable for the interaction's full ten minutes, unbounded and unauthenticated, with every replay driving a server-side POST to the developer's token endpoint and one audit row into their org \u2014 an amplifier against a third party. The interaction is now spent on **every** terminal outcome, refusals included, which closes the replay itself; the limiter is the ceiling on how fast the attempts may arrive at all. It is per ip and sized for a browser, so a person completing a sign-in never meets it. `429 rate_limited` is the one `apiError` this route can answer, and it is not a sign-in outcome \u2014 it is a refusal to begin the work, which is why it does not ride back to the app as an `?error=`. \n\nThe query is the **provider's** rather than a Fleetless shape, and `clientOidcCallbackQuery` describes it **without being strict**: `state` always, `code` on success, `error` and `error_description` on the provider's own refusal, and whatever else that provider adds \u2014 RFC 9207's `iss`, a `session_state`, a vendor field. Refusing those would refuse conforming providers, the trap `POST /mcp/oauth/register` documents avoiding. `state` is the required field because it is the only one Fleetless minted. \n\n**It lists `rate_limited` and no other code, because every sign-in outcome it has is a redirect.** Success and failure alike are a `302` to the app's own `redirect_uri`: `?code=\u2026&state=\u2026` when a session was resolved, `?error=<clientOidcErrorCode>&state=\u2026` when it was not, so the app renders its own message and can bind either answer to the request it started. Fleetless shows an app user no page. \n\n**The one exception is a `state` that resolves to no interaction** \u2014 unknown, hand-edited, or past its ten minutes. Then there is no confirmed redirect target to carry the answer to, and bouncing a browser to an unvalidated one is the hole the whole flow is arranged to avoid, so the cloud renders an HTML problem page at `400`. That is the only Fleetless-rendered surface an app user can reach. It is HTML rather than an `apiError`, which is why no code is listed: a code here would document an envelope no caller receives, and this manifest's other HTML pages (`GET /mcp/oauth/interaction/:id`, `GET /console/oauth/interaction/:id`) say their status in prose for the same reason."
6846
6721
  },
6847
6722
  {
6848
6723
  method: "POST",
@@ -6862,7 +6737,7 @@ var ROUTES = [
6862
6737
  transport: "http",
6863
6738
  notes: "The second half of the app's own PKCE: the `code` from the callback redirect plus the `code_verifier` for the challenge `start` carried. Sixty seconds, single-use, and worth nothing to whoever intercepted the redirect without the verifier. \n\n**One refusal for every code that does not work: `410 token_spent`** \u2014 unknown, past its sixty seconds, already exchanged, or presented with a verifier that does not match. There is one code because this code **is a credential**: telling the four apart would say whether a given value ever existed, and the recovery is the same in all four \u2014 start the sign-in again. The MCP interaction routes collapse their four states the same way and for the same reason, and answer `interaction_expired` rather than this code \u2014 the difference is what the value is, not how vague the answer is: a mailed one-time code is a credential, an interaction id names a pending request, and the two deserve different advice on the app's own page."
6864
6739
  },
6865
- /* --------------------------- the app's own MCP consent screen (D7) */
6740
+ /* ------------------------------- the app's own MCP consent screen */
6866
6741
  {
6867
6742
  method: "GET",
6868
6743
  path: "/api/client/mcp/interactions/:id",
@@ -6897,7 +6772,7 @@ var ROUTES = [
6897
6772
  response: clientMcpInteractionDecisionResponse,
6898
6773
  errors: [...CLIENT_GUARD, "rate_limited", "interaction_expired", "mcp_disabled"],
6899
6774
  transport: "http",
6900
- notes: "The person is already signed in **at the app**, by whatever means that app uses, and this is the app telling Fleetless what they decided. Fleetless never sees that sign-in, which is D7 in one sentence. \n\nThe guard admits all three caller kinds and the handler takes one: a developer bearer or a server key reaching this is `401 unauthorized`, because a consent is a person's and a server key is not a person \u2014 the same shape `POST /api/client/password/change` has. `403 mcp_disabled` is the app's switch, re-read here as it is on every request \u2014 and it is the one refusal on this surface that names the switch, because reaching it needs an app-user session of that very app. \n\n**An interaction of ANOTHER app answers `410 interaction_expired`, not `403`.** An interaction of one app cannot be decided with a session from another \u2014 that is what stops a developer running two apps from letting one speak for the other \u2014 but saying so with a distinct code would tell any bearer holder that the id names a real, live interaction somewhere else, which is the existence answer the shared `410` exists to withhold. Unknown, expired, already decided, an interaction of the central flow, and one belonging to a different app are one status and one body. Approve and deny spend an interaction alike, so the second call gets it whichever route made the first. \n\n**Rate limited per app user, unlike the read.** The read is a public document about a request the server already holds; this one spends something, and a decision is the one thing a leaked interaction id would be worth hammering for. The limit is on the signed-in account rather than on the ip, because that is what the caller has had to prove. \n\n**The answer is a redirect target, not a redirect.** `redirect_to` is the MCP client's own callback carrying the authorization code, and the app's page sends the browser there. The app is holding that browser and Fleetless is answering its JSON call, so a `302` here would be a redirect on the wrong request. Approving records the grant for this user and this client, which is what a later `already_granted` reads back."
6775
+ notes: "The person is already signed in **at the app**, by whatever means that app uses, and this is the app telling Fleetless what they decided. Fleetless never sees that sign-in. \n\nThe guard admits all three caller kinds and the handler takes one: a developer bearer or a server key reaching this is `401 unauthorized`, because a consent is a person's and a server key is not a person \u2014 the same shape `POST /api/client/password/change` has. `403 mcp_disabled` is the app's switch, re-read here as it is on every request \u2014 and it is the one refusal on this surface that names the switch, because reaching it needs an app-user session of that very app. \n\n**An interaction of ANOTHER app answers `410 interaction_expired`, not `403`.** An interaction of one app cannot be decided with a session from another \u2014 that is what stops a developer running two apps from letting one speak for the other \u2014 but saying so with a distinct code would tell any bearer holder that the id names a real, live interaction somewhere else, which is the existence answer the shared `410` exists to withhold. Unknown, expired, already decided, an interaction of the central flow, and one belonging to a different app are one status and one body. Approve and deny spend an interaction alike, so the second call gets it whichever route made the first. \n\n**Rate limited per app user, unlike the read.** The read is a public document about a request the server already holds; this one spends something, and a decision is the one thing a leaked interaction id would be worth hammering for. The limit is on the signed-in account rather than on the ip, because that is what the caller has had to prove. \n\n**The answer is a redirect target, not a redirect.** `redirect_to` is the MCP client's own callback carrying the authorization code, and the app's page sends the browser there. The app is holding that browser and Fleetless is answering its JSON call, so a `302` here would be a redirect on the wrong request. Approving records the grant for this user and this client, which is what a later `already_granted` reads back."
6901
6776
  },
6902
6777
  {
6903
6778
  method: "POST",
@@ -6934,7 +6809,7 @@ var ROUTES = [
6934
6809
  response: mcpConsentGrantListResponse,
6935
6810
  errors: [...CLIENT_GUARD],
6936
6811
  transport: "http",
6937
- notes: "**So the developer's app can offer a \"connected apps\" screen of its own**, which is the only place an end user could ever be shown this: Fleetless renders no page for an app's users (D2), and the console is the developer's tool rather than their customers'. \n\nThe answer is about the bearer's own account and takes no user id \u2014 there is no id to pass and therefore nothing to pass the wrong one. The guard admits all three caller kinds because it is shared, and the handler takes one: a developer bearer or a server key is `401 unauthorized`, the shape `POST /api/client/password/change` has, because a consent is a person's and a server key is not a person. \n\n**Every `client_name` is unverified**, on every row: dynamic registration takes no credential, so the name is text the client chose about itself and `client_name_verified` is the literal `false`. A screen that renders it as an identity is showing somebody a string an attacker picked, and this list is read long after the moment of approval, when nobody remembers what they clicked. **Withdrawn grants are absent**, not listed as withdrawn. \n\n**Not rate limited and not gated on the app's MCP switch.** It reads one small table for one account, and a person must be able to see and end what they agreed to even after a developer switches MCP off \u2014 a withdrawal door that closes with the feature is a door that is shut exactly when somebody wants it."
6812
+ notes: "**So the developer's app can offer a \"connected apps\" screen of its own**, which is the only place an end user could ever be shown this: Fleetless renders no page for an app's users, and the console is the developer's tool rather than their customers'. \n\nThe answer is about the bearer's own account and takes no user id \u2014 there is no id to pass and therefore nothing to pass the wrong one. The guard admits all three caller kinds because it is shared, and the handler takes one: a developer bearer or a server key is `401 unauthorized`, the shape `POST /api/client/password/change` has, because a consent is a person's and a server key is not a person. \n\n**Every `client_name` is unverified**, on every row: dynamic registration takes no credential, so the name is text the client chose about itself and `client_name_verified` is the literal `false`. A screen that renders it as an identity is showing somebody a string an attacker picked, and this list is read long after the moment of approval, when nobody remembers what they clicked. **Withdrawn grants are absent**, not listed as withdrawn. \n\n**Not rate limited and not gated on the app's MCP switch.** It reads one small table for one account, and a person must be able to see and end what they agreed to even after a developer switches MCP off \u2014 a withdrawal door that closes with the feature is a door that is shut exactly when somebody wants it."
6938
6813
  },
6939
6814
  {
6940
6815
  method: "DELETE",
@@ -6952,7 +6827,7 @@ var ROUTES = [
6952
6827
  response: null,
6953
6828
  errors: [...CLIENT_GUARD],
6954
6829
  transport: "http",
6955
- notes: "The person's own door, beside the developer's `DELETE /api/apps/:id/users/:userId/mcp-grants/:clientId`. It acts on the bearer's own account and on no other \u2014 the path carries a client and never a subject \u2014 so there is no user for a caller to name and none to confuse. The shared guard admits all three caller kinds and the handler takes one: a developer bearer or a server key is `401 unauthorized`, because withdrawing a consent is the same person's act as giving it. Audited as `app_user.mcp_grant_revoked`, with the app user themselves as the actor. \n\n**`204` whether or not there was anything to withdraw.** A client id this account never approved, and one it withdrew a minute ago, both answer the end state that was asked for: a `404` would tell the caller which clients some account has connected, and would make the ordinary double-click a failure. Only a withdrawal that ended a standing agreement is audited. \n\n**It does not end an MCP session already running.** That consent minted a fifteen-minute access token, and the MCP transport checks it against the account rather than against this table, so a session in flight survives until it expires. Nothing can extend it \u2014 this authorization server issues no refresh tokens \u2014 and the next authorization asks again. An app that needs a client cut off **now** blocks the account, which ends every session that account holds rather than this client's alone."
6830
+ notes: "The person's own door, beside the developer's `DELETE /api/apps/:id/users/:userId/mcp-grants/:clientId`. It acts on the bearer's own account and on no other \u2014 the path carries a client and never a subject \u2014 so there is no user for a caller to name and none to confuse. The shared guard admits all three caller kinds and the handler takes one: a developer bearer or a server key is `401 unauthorized`, because withdrawing a consent is the same person's act as giving it. Audited as `app_user.mcp_grant_revoked`, with the app user themselves as the actor. \n\n**`204` whether or not there was anything to withdraw.** A client id this account never approved, and one it withdrew a minute ago, both answer the end state that was asked for: a `404` would tell the caller which clients some account has connected, and would make the ordinary double-click a failure. Only a withdrawal that ended a standing agreement is audited. \n\n**It ends a session already running, at that client's very next call.** The app's MCP endpoint reads this table on every request and keys the check on the `client_id` the access token carries, so the withdrawn client is answered `401` with a challenge and has to ask this person again. The token it holds is still unexpired \u2014 up to fifteen minutes are left on it \u2014 and is refused anyway. Nothing else stops: this ends one client, not the account, which is what blocking would end."
6956
6831
  },
6957
6832
  /* ------------------------------------------------------------- robots */
6958
6833
  {
@@ -7445,7 +7320,7 @@ var ROUTES = [
7445
7320
  response: jobRunListResponse,
7446
7321
  errors: [...CLIENT_GUARD, "invalid_uuid", "not_found", "capability_required", "validation_error"],
7447
7322
  transport: "http",
7448
- notes: "Needs the `action_history` capability, and **this route is what makes that switch mean something** \u2014 it was unkeepable while nothing durable recorded what had run. Two residuals worth stating rather than implying away. `history` is a syntactically valid slug and Fastify matches a static segment first, so a robot with a service literally slugged `history` can no longer be **read** through `GET /api/robots/:id/jobs/:slug`; invoking, cancelling and the listing are unaffected. And a run row names its actor by email address, so an end user holding this capability learns which other people have been driving the machine. `robot_id` in the query is shared with the org-wide read; a *different* one here is refused rather than quietly answered about the robot in the path."
7323
+ notes: "Needs the `action_history` capability, and **this route is what makes that switch mean something** \u2014 it was unkeepable while nothing durable recorded what had run. Two residuals worth stating rather than implying away. `history` is a syntactically valid slug, and a static segment matches before a parameter, so a robot with a service literally slugged `history` can no longer be **read** through `GET /api/robots/:id/jobs/:slug`; invoking, cancelling and the listing are unaffected. And a run row names its actor by email address, so an end user holding this capability learns which other people have been driving the machine. `robot_id` in the query is shared with the org-wide read; a *different* one here is refused rather than quietly answered about the robot in the path."
7449
7324
  },
7450
7325
  {
7451
7326
  method: "POST",
@@ -7947,7 +7822,7 @@ var ROUTES = [
7947
7822
  response: asset,
7948
7823
  errors: ["unauthorized", "rate_limited", "asset_too_large", "validation_error", "not_found", "quota_exceeded", "bad_request"],
7949
7824
  transport: "http",
7950
- notes: "The body is the **raw file bytes**, not JSON, so it has no request schema; everything about the file \u2014 its kind, its name, its sync id and its announced size \u2014 rides in the `x-fleetless-asset-*` headers `ASSET_UPLOAD_HEADERS` names. The credential is a short-lived upload token minted by `POST /api/robots/:id/assets/sync`, verified in a `preParsing` hook so a refusal precedes the work rather than following it: a `preHandler` would already have buffered the whole file. The announced size is refused there too, before a single byte is read \u2014 it is an announcement and not a proof, so it only ever rejects early and never accepts early, and a body that lies small is still caught by the real length check. Past both, Fastify's own body limit answers a bare `413 bad_request` with neither ceiling nor size in it. Rate limited per robot inside that same hook, which is why `rateLimited` is `false`: there is no rate-limiting preHandler registered on this route."
7825
+ notes: "The body is the **raw file bytes**, not JSON, so it has no request schema; everything about the file \u2014 its kind, its name, its sync id and its announced size \u2014 rides in the `x-fleetless-asset-*` headers `ASSET_UPLOAD_HEADERS` names. The credential is a short-lived upload token minted by `POST /api/robots/:id/assets/sync`, verified in a `preParsing` hook so a refusal precedes the work rather than following it: a `preHandler` would already have buffered the whole file. The announced size is refused there too, before a single byte is read \u2014 it is an announcement and not a proof, so it only ever rejects early and never accepts early, and a body that lies small is still caught by the real length check. Past both, the server's own body limit answers a bare `413 bad_request` with neither ceiling nor size in it. Rate limited per robot inside that same hook, which is why `rateLimited` is `false`: there is no rate-limiting preHandler registered on this route."
7951
7826
  },
7952
7827
  /* ------------------------------------ realtime and bridge transports */
7953
7828
  {
@@ -8464,7 +8339,7 @@ function createRealtimeCommandTransport(channel) {
8464
8339
  return {
8465
8340
  // Declared `async` deliberately, unlike `cancel`/`publish` below: it is
8466
8341
  // the only one of the three that can refuse *before* sending anything
8467
- // (resolveLocalWaitMs's `invalid_option`, D3a), and every caller of
8342
+ // (`resolveLocalWaitMs`'s `invalid_option`), and every caller of
8468
8343
  // this interface — starting with this file's own `sendCommand` callers
8469
8344
  // — is entitled to assume `CommandTransport.invoke` always returns a
8470
8345
  // promise rather than throwing synchronously. Without `async` here, a
@@ -8484,7 +8359,7 @@ function createRealtimeCommandTransport(channel) {
8484
8359
  };
8485
8360
  return sendCommand(channel, frame, { timeoutMs });
8486
8361
  },
8487
- // Declared `async` for the same reason `invoke` above is (D3a, D6):
8362
+ // Declared `async` for the same reason `invoke` above is:
8488
8363
  // `assertValidJobId` can throw before a frame is ever built, and a
8489
8364
  // caller of this interface is entitled to a rejected promise, never a
8490
8365
  // thrown exception.