@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.js CHANGED
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  // src/errors.ts
2
3
  var SDK_ERROR_CODES = [
3
4
  "no_session",
@@ -113,7 +114,7 @@ var HttpClient = class {
113
114
  * RequestOptionsFor<P>` would type-check by construction — the cast
114
115
  * bypasses the very check this exists to add, which is the identical
115
116
  * "special case that quietly exempts calls from the general rule" shape
116
- * this file has already been caught by once. Every current call site to a route
117
+ * this file has already made once. Every current call site to a route
117
118
  * with no body already passes `{}` explicitly for exactly this reason.
118
119
  */
119
120
  async request(path, options) {
@@ -410,7 +411,7 @@ function createAssetsApi(http) {
410
411
  };
411
412
  }
412
413
 
413
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/common.js
414
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/common.js
414
415
  import { z } from "zod";
415
416
  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.";
416
417
  var slug = z.string().min(2).max(63).regex(/^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$/, SLUG_RULE);
@@ -434,7 +435,7 @@ var applyError = z.object({
434
435
  details: z.record(z.string(), z.unknown()).optional()
435
436
  });
436
437
 
437
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/mcp.js
438
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/mcp.js
438
439
  import { z as z2 } from "zod";
439
440
  function mcpAppEndpointPath(appIdentifier2) {
440
441
  return `/mcp/${appIdentifier2}`;
@@ -483,10 +484,10 @@ var mcpRolePreviewResponse = z2.object({
483
484
  });
484
485
  var MCP_ASSET_LINK_TTL_MS = 15 * 60 * 1e3;
485
486
 
486
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/protocol.js
487
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/protocol.js
487
488
  import { z as z8 } from "zod";
488
489
 
489
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/assets.js
490
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/assets.js
490
491
  import { z as z3 } from "zod";
491
492
  var assetKind = z3.enum(["urdf", "mesh", "texture", "other"]);
492
493
  var asset = z3.object({
@@ -501,7 +502,7 @@ var asset = z3.object({
501
502
  * against their own workspace, and matching is the whole job when a sync
502
503
  * comes back incomplete.
503
504
  *
504
- * **The naming rule for a file nothing in the URDF names (W7a, D2).** A
505
+ * **The naming rule for a file nothing in the URDF names.** A
505
506
  * `.dae` carries its own image references — `<init_from>textures/skin.png`
506
507
  * — resolved by the renderer against *the `.dae`'s own directory*, and no
507
508
  * `package://` URI for them appears anywhere in the URDF. The rule is:
@@ -509,12 +510,11 @@ var asset = z3.object({
509
510
  * name = the .dae's package:// URI, directory part,
510
511
  * joined with the internal reference, normalized.
511
512
  *
512
- * So `package://rx1_description/meshes/arm.dae` referencing
513
+ * So `package://robot_description/meshes/arm.dae` referencing
513
514
  * `textures/skin.png` uploads as
514
- * `package://rx1_description/meshes/textures/skin.png`.
515
+ * `package://robot_description/meshes/textures/skin.png`.
515
516
  *
516
- * **This is the design's single point of failure and it is stated before
517
- * anything is built against it.** three.js resolves that internal reference
517
+ * **Renderers depend on this rule holding.** three.js resolves that internal reference
518
518
  * relative to wherever it loaded the `.dae` from and asks the loading
519
519
  * manager for the result; the client can only answer if the asset's name
520
520
  * still carries the same **relative tail** (`textures/skin.png`) that the
@@ -525,9 +525,8 @@ var asset = z3.object({
525
525
  *
526
526
  * A reference that escapes its package (`../../etc/passwd`) is **not**
527
527
  * renamed into something harmless — it is refused at the producer, by the
528
- * same containment check W7's K2 fix applied to `package://` resolution.
529
- * Two identical rules, one of which is enforced and one of which is
530
- * documented, is how W7's traversal happened in the first place.
528
+ * same containment check that guards `package://` resolution. Two identical
529
+ * rules, one enforced and one only documented, is how a traversal gets in.
531
530
  */
532
531
  name: z3.string().min(1).max(500).meta({
533
532
  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."
@@ -561,18 +560,17 @@ var urdfCompleteness = z3.object({
561
560
  description: "How many distinct meshes the URDF references."
562
561
  }),
563
562
  /**
564
- * **Was fehlt, und wovon (W9b, DEF-081).**
563
+ * **What is missing, and what kind of thing it was.**
565
564
  *
566
- * Vorher ein blankes `string[]`. Die Console meldete daraufhin *„N meshes
567
- * missing from the workspace"* — auch für eine fehlende **Textur**, während
568
- * `mesh_count` daneben eine andere Zahl nannte: zwei Angaben über denselben
569
- * Gegenstand, die einander widersprechen.
565
+ * Each entry carries its element rather than only its URI. Without that a
566
+ * client can only report every entry as a missing mesh, which contradicts
567
+ * `mesh_count` printed beside it — two statements about the same subject
568
+ * that disagree.
570
569
  *
571
- * Die Cloud wusste es die ganze Zeit: `extractReferencesByElement` markiert
572
- * jede Referenz mit ihrem Element und `buildAssetListResponse` warf die
573
- * Markierung wieder weg. **Die Antwort im Client zu raten wäre genau die
574
- * „neue Kopie", die die Registerzeile ausdrücklich ablehnt** — eine zweite
575
- * Herleitung derselben Tatsache, die von der ersten abweichen kann.
570
+ * The producer knows the element when it extracts the reference, so it
571
+ * travels with it. A client that re-derived it from the file extension
572
+ * would be a second derivation of the same fact, free to diverge from the
573
+ * first.
576
574
  */
577
575
  missing: z3.array(z3.object({
578
576
  uri: z3.string().min(1).max(500).meta({
@@ -620,23 +618,20 @@ var assetFailure = z3.object({
620
618
  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`."
621
619
  }),
622
620
  /**
623
- * **Die zwei Zahlen, und warum `too_large` eine eigene Art ist (W9b).**
621
+ * **The two numbers, and why `too_large` is a kind of its own.**
624
622
  *
625
- * `refused` bedeutet *„nie versucht, weil eine Decke des Erzeugers erreicht
626
- * wurde"* — das passt auf eine Datei, die wegen ihrer Größe gar nicht erst
627
- * gelesen wurde, **und ebenso auf die Sammel-Sentinel**, mit der ein Sync
628
- * aufhört, einzelne Fehler zu benennen. Beides unter eine Art zu legen wäre
629
- * derselbe Fehler, den W9a eine Welle zuvor ausgeräumt hat: zwei Fakten auf
630
- * einem Schlüssel, von denen jeder den anderen überschreibt.
623
+ * `refused` means *never attempted, because a producer-side ceiling was
624
+ * hit*. That fits a file skipped for its size **and** the single collective
625
+ * entry a sync emits when it stops naming individual failures. Filing both
626
+ * under one kind puts two facts on one key, each overwriting the other.
631
627
  *
632
- * Und ein Grund ohne Zahlen ist kein Grund, mit dem jemand etwas anfangen
633
- * kann. *„Zu groß"* beantwortet nicht, ob das Mesh zu verkleinern ist oder
634
- * die Grenze zu heben — `limit_bytes` und `size_bytes` tun es.
628
+ * A reason without numbers is not one a caller can act on. *"Too large"*
629
+ * does not answer whether to shrink the mesh or raise the limit;
630
+ * `limit_bytes` and `size_bytes` do.
635
631
  *
636
- * Abwesend für jede andere Art — ein erzwungenes `details: null` auf jedem
637
- * `unresolvable` kauft nichts. Die Paarung ist unten **erzwungen**, nicht
638
- * beschrieben: ein Feld, dessen Regel nur im Kommentar steht, ist eine
639
- * Bitte.
632
+ * Absent for every other kind — a forced `details: null` on every
633
+ * `unresolvable` buys nothing. The pairing is **enforced** below, not merely
634
+ * described: a field whose rule lives only in a comment is a request.
640
635
  */
641
636
  details: assetTooLargeDetails.nullish().meta({
642
637
  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."
@@ -663,48 +658,28 @@ var assetSyncStatus = z3.object({
663
658
  description: "How many files this sync set out to transfer. It is `0` until the producer has finished working out what there is."
664
659
  }),
665
660
  /**
666
- * **Every entry says *why*, because reconciliation could not work without
667
- * it and a developer could not read it without it** (W7a review, André's
668
- * decision to fix rather than defer).
661
+ * **Every entry says *why*.**
669
662
  *
670
- * It was a flat `string[]`, and **six producers wrote three different facts
671
- * into it indistinguishably**: a reference that resolves to nothing in the
672
- * workspace, a file that exists and whose transfer failed, and — since R9's
673
- * ceiling — one that was never attempted at all. The cost was paid twice
674
- * over. Reconciliation cannot tell *"no longer referenced"* from
675
- * *"referenced and not delivered"*, so N14 had to decline reconciling **any**
676
- * partial sync, leaving legitimately-removed assets stored and charged until
677
- * the next clean one. And the console prints the whole array under *"these
678
- * meshes could not be resolved"*, so a URDF upload failure — which arrives
679
- * as the literal `robot_description` — is shown to a developer as a mesh
680
- * they should go and find.
663
+ * A flat `string[]` cannot carry three different facts distinguishably: a
664
+ * reference that resolves to nothing in the workspace, a file that exists
665
+ * and whose transfer failed, and one that was never attempted at all.
681
666
  *
682
- * `unresolvable` is the only kind reconciliation may drop: it is the only
683
- * one that means *this will not come back*. `upload_failed` and `refused`
684
- * both mean *we meant to provide this and did not*, which is the distinction
685
- * the union needs and the field could not carry.
667
+ * The distinction is what makes reconciliation possible. `unresolvable` is
668
+ * the only kind that means *this will not come back*, so it is the only kind
669
+ * an unreferenced-asset sweep may act on. `upload_failed` and `refused` both
670
+ * mean *this was meant to be provided and was not*, and dropping the asset
671
+ * on either would delete something still wanted.
686
672
  *
687
- * **Bounded, and the bound is a rule this file already wrote down one field
688
- * over** (Kassandra-W7a, W7a review). `asset.name` is `max(500)`; the same
689
- * names travelling here had no per-entry cap and no array cap at all.
673
+ * **Bounded, per entry and in total.** One mesh reference can expand into a
674
+ * list bounded only by the text of the `.dae` it points at, and the whole
675
+ * status travels in one WebSocket frame with a maximum payload. Without a
676
+ * cap the outcome is not a dropped frame but the robot's socket closed
677
+ * mid-sync by a file in its own workspace. At most 1000 entries of at most
678
+ * 500 bytes is about 0.5 MiB of names, comfortably inside that frame.
690
679
  *
691
- * Why it matters became reachable in W7a. Before R6 an unresolvable
692
- * reference was silently dropped, so this array could only grow with files
693
- * that existed and failed to upload — bounded by the workspace. Once a
694
- * `.dae`'s internal references are reported, **one mesh reference expands
695
- * into a list bounded only by that file's own text.** Measured: a
696
- * 2,120,745-byte `.dae` with 17,331 unresolvable `<init_from>` refs produces
697
- * a terminal frame of 2,097,184 bytes — **32 bytes over
698
- * `MAX_WS_PAYLOAD_BYTES`** — and `ws` enforces `maxPayload` before the frame
699
- * is delivered, so the outcome is not a dropped frame but **the robot's
700
- * socket closed, mid-sync, by a file in its own workspace.**
701
- *
702
- * A producer that hits its own ceiling reports **one** entry saying so
703
- * rather than growing the list — the discipline gate step 5 already demands
704
- * of the upload rate limit: *fail naming the limit, rather than silently
705
- * reporting resolvable meshes as missing.*
706
- *
707
- * 1000 x 500 bytes is ~0.5 MiB of names, comfortably inside a 2 MiB frame.
680
+ * A producer that reaches its own ceiling reports **one** entry saying so
681
+ * rather than growing the list: fail naming the limit, rather than silently
682
+ * reporting resolvable meshes as missing.
708
683
  */
709
684
  failed: z3.array(assetFailure).max(1e3).meta({
710
685
  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."
@@ -724,14 +699,13 @@ var assetListResponse = z3.object({
724
699
  description: "Every asset stored for this robot: the URDF, the meshes it references, and the textures those paint with."
725
700
  }),
726
701
  /**
727
- * Der gerade laufende Sync, oder `null` (W9b, DEF-147).
702
+ * The sync running right now, or `null`.
728
703
  *
729
- * **Der Fall, für den das hier steht, ist der Neuladen-Fall.** Die Console
730
- * hielt die `sync_id` nur im Speicher; ein Reload verlor die Fortschritts-
731
- * anzeige, und der Zustand war serverseitig da, über
732
- * `GET .../assets/sync/<id>` abfragbar — nur erreichte ihn niemand mehr, der
733
- * die id nicht aufgehoben hatte. Eine Seite, die frisch lädt, drückt keinen
734
- * Knopf; sie fragt diese Liste. Also muss die Liste es sagen.
704
+ * **This field exists for the reload case.** A client that holds the
705
+ * `sync_id` only in memory loses its progress display on a refresh, and the
706
+ * state is still there server-side under `GET .../assets/sync/<id>` —
707
+ * unreachable to anyone who did not keep the id. A page that loads fresh
708
+ * presses no button; it asks this list, so this list has to say.
735
709
  */
736
710
  active_sync: assetSyncStatus.nullable().meta({
737
711
  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."
@@ -741,30 +715,20 @@ var assetListResponse = z3.object({
741
715
  }),
742
716
  /**
743
717
  * What the connected bridge says it *could* transfer, which is deliberately
744
- * separate from what has been transferred (§4.6: the bridge "meldet nur
745
- * Verfügbarkeit"). `null` when no bridge is connected — distinct from
746
- * `false`, because "no robot is online to ask" and "the robot has no URDF"
747
- * send a developer to two different places.
748
- *
749
- * **All three states are reachable as of W7a (R7).** They were not: the bridge
750
- * used to report availability from a subscription callback, which fires only
751
- * when a publisher *sends* something, so it could notice presence and never
752
- * absence — a robot that lost its URDF left the cloud holding the last thing
753
- * it heard, forever, and `true` was sticky. The fix is an **active**
754
- * `count_publishers` query on the bridge's own timer.
718
+ * separate from what has been transferred. `null` when no bridge is
719
+ * connected — distinct from `false`, because "no robot is online to ask" and
720
+ * "the robot has no URDF" send a developer to two different places.
755
721
  *
756
- * **What a consumer still needs to know is the clock, not the gap.** An
757
- * ungraceful loss — the publisher process killed rather than shut down — is
758
- * noticed on **DDS's liveliness timeout**, not on the bridge's check
759
- * interval. Measured against a real bridge: ~1.6 s when the publisher calls
760
- * `destroy_node()`, **~19 s when it is `SIGKILL`ed**. So `true` can outlive
761
- * the truth by some seconds after a crash, and no amount of polling on our
762
- * side shortens it.
722
+ * The bridge answers by actively counting publishers on its own timer, so
723
+ * both the appearance and the disappearance of a robot description are
724
+ * noticed. A callback-driven answer can only see presence.
763
725
  *
764
- * The sticky-`true` gap was found by Rosie-W7 checking her own work against
765
- * the camera-health row of identical shape; the DDS clock was measured by
766
- * Rosie-W7a closing it, and this comment was still describing the gap a wave
767
- * after it was fixed (Momus-W7a, W7a review).
726
+ * **What a consumer needs to know is the clock.** An ungraceful loss — the
727
+ * publisher process killed rather than shut down — is noticed on DDS's
728
+ * liveliness timeout, not on the bridge's check interval. A clean
729
+ * `destroy_node()` is visible in a second or two; a killed process can take
730
+ * around twenty. So `true` can outlive the truth by some seconds after a
731
+ * crash, and no amount of polling shortens it.
768
732
  */
769
733
  urdf_available: z3.boolean().nullable().meta({
770
734
  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.'
@@ -777,14 +741,14 @@ var missingAssetQuery = z3.object({
777
741
  }).meta({ description: "The one optional parameter of the missing-asset placeholder; it names the reference in the refusal." });
778
742
  var assetSyncBusyDetails = z3.object({
779
743
  sync_id: z3.uuid(),
780
- /** Wann er begann — damit „läuft noch" von „hängt seit einer Stunde" unterscheidbar ist. */
744
+ /** When it started, so "still running" is distinguishable from "stuck for an hour". */
781
745
  started_at_ms: z3.number().int().nonnegative()
782
746
  });
783
747
 
784
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/config.js
748
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/config.js
785
749
  import { z as z5 } from "zod";
786
750
 
787
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/alerts.js
751
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/alerts.js
788
752
  import { z as z4 } from "zod";
789
753
  var alertRowCondition = z4.discriminatedUnion("kind", [
790
754
  z4.strictObject({
@@ -814,7 +778,7 @@ var datapointAlertRow = z4.object({
814
778
  severity: alertSeverity,
815
779
  condition: alertRowCondition,
816
780
  state: alertState,
817
- /** `null` only until the first evaluation writes a state; every alert is created `ok` (D2), so in practice this is set from creation onward. */
781
+ /** `null` only until the first evaluation writes a state; every alert is created `ok`, so in practice this is set from creation onward. */
818
782
  state_since: z4.iso.datetime().nullable(),
819
783
  /**
820
784
  * The value at the alert's last state transition — written only when the
@@ -854,7 +818,7 @@ var putDatapointDisplayRequest = z4.object({
854
818
  y_max: z4.number().finite().nullable()
855
819
  }).strict();
856
820
 
857
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/config.js
821
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/config.js
858
822
  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:`.";
859
823
  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:`.";
860
824
  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.";
@@ -1094,10 +1058,9 @@ var datapointAlert = strictObject({
1094
1058
  * `getInsertTextForProperty` (`yaml.worker.js:8520`) takes
1095
1059
  * `defaultSnippets[0].body` only when a node carries **exactly one**
1096
1060
  * snippet, so accepting `condition` from the key list writes the bare key
1097
- * here where every other node this wave touched writes its whole block.
1098
- * The two stay anyway: the value position — a developer who has written
1099
- * `condition:` and pressed ⏎ — is where the question "what goes here?" is
1100
- * actually asked, and that is the position this wave exists to answer.
1061
+ * here where every other node writes its whole block. The two stay anyway:
1062
+ * the value position — a developer who has written `condition:` and pressed
1063
+ * ⏎ — is where the question "what goes here?" is actually asked.
1101
1064
  * Merging them into one would buy back the key completion by deleting the
1102
1065
  * choice the schema deliberately does not name, which is the worse trade;
1103
1066
  * anyone tempted to make it should change the key-completion behaviour
@@ -1216,9 +1179,9 @@ var NUMERIC_DATAPOINT_SNIPPET = {
1216
1179
  /**
1217
1180
  * The quotes inside `unit` are part of the inserted text and are not
1218
1181
  * decoration. A body string is written into the document verbatim, and `%`
1219
- * is a YAML directive indicator: measured with `yaml` 2.9.0, `unit: %` is a
1220
- * **syntax error** ("Plain value cannot start with directive indicator
1221
- * character %") while `unit: "%"` parses to `%`. Nothing between here and
1182
+ * is a YAML directive indicator: `unit: %` is a **syntax error** ("Plain
1183
+ * value cannot start with directive indicator character %") while
1184
+ * `unit: "%"` parses to `%`. Nothing between here and
1222
1185
  * the buffer quotes a scalar for us.
1223
1186
  */
1224
1187
  numeric: { scale: 100, unit: '"%"', decimals: 1 },
@@ -1664,15 +1627,14 @@ var cameraSource = z5.discriminatedUnion("kind", [
1664
1627
  * `rosTypeName` accepts either, publish accepts either, no diagnostic
1665
1628
  * fires anywhere, and the bridge then subscribes with the wrong type and
1666
1629
  * delivers no frames. A snippet supplying a wrong answer where it could
1667
- * have supplied a question is this project's *check that cannot fire*,
1668
- * arriving through a hint the developer trusts.
1630
+ * have supplied a question is a defect arriving through a hint the
1631
+ * developer trusts.
1669
1632
  *
1670
1633
  * `kind: 'ros'` stays a literal, because the branch really does fix it.
1671
1634
  *
1672
- * Measured through the actual pipeline rather than assumed, because
1673
- * choice syntax is the one construct here that three layers must each
1635
+ * Choice syntax is the one construct here that three layers must each
1674
1636
  * pass through unharmed: yaml-language-server's `stringifyObject` emits
1675
- * the body verbatim, and monaco-editor 0.52.2's `SnippetParser` parses
1637
+ * the body verbatim, and monaco-editor's `SnippetParser` parses
1676
1638
  * `${2|a,b|}` into a placeholder carrying both options whose
1677
1639
  * `toString()` — the text on the buffer before anyone chooses — is the
1678
1640
  * first one. So a developer who tabs past this gets a document
@@ -1691,16 +1653,12 @@ var cameraSource = z5.discriminatedUnion("kind", [
1691
1653
  description: "Selects the RTSP source: this camera then carries `url`, and optionally `transport` and `credentials`."
1692
1654
  }),
1693
1655
  /**
1694
- * Scheme-constrained deliberately. The playbook drafted `z.string().url()`
1695
- * here and the shipped contract was `z.string().min(1).max(2048)` — nobody
1696
- * recorded the change, and the W6 review found the consequence: the bridge
1697
- * opens these with libraries that honour `file:` and `ftp:`, so an
1698
- * unconstrained URL turns a configuration document into an arbitrary
1699
- * local-file read on the robot, with the two distinct failure codes
1700
- * doubling as a file-existence oracle. Spec §7.6 is ROS-pure exposure with
1701
- * no shell or http features; that rule came back by omission rather than
1702
- * by intent. The bridge re-checks this too — a robot must not become a
1703
- * file server because a validator changed.
1656
+ * Scheme-constrained deliberately. The bridge opens these with libraries
1657
+ * that honour `file:` and `ftp:`, so an unconstrained URL turns a
1658
+ * configuration document into an arbitrary local-file read on the robot,
1659
+ * with the two distinct failure codes doubling as a file-existence oracle.
1660
+ * The bridge re-checks this too — a robot must not become a file server
1661
+ * because a validator changed.
1704
1662
  */
1705
1663
  url: z5.string().min(1).max(2048).regex(/^rtsps?:\/\//i, RTSP_URL_RULE).meta({
1706
1664
  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.",
@@ -1738,8 +1696,8 @@ var cameraSource = z5.discriminatedUnion("kind", [
1738
1696
  /**
1739
1697
  * The host and the path this branch's own snippet body inserts, and the
1740
1698
  * URL its rule sentence names — one answer to "what goes here?", not a
1741
- * third. The sibling `rtsp` url had an `examples` from the first day and
1742
- * this position was the format's only silent URL (§1.1).
1699
+ * third. Every URL position in the format carries an example; a silent
1700
+ * one is the position a developer has to guess at.
1743
1701
  */
1744
1702
  examples: ["http://cam-1.plant.local/video.mjpg"]
1745
1703
  }),
@@ -1764,16 +1722,14 @@ var cameraSource = z5.discriminatedUnion("kind", [
1764
1722
  * on the robot, never by the cloud.
1765
1723
  *
1766
1724
  * Constrained to `/dev/` for the same reason the `rtsp` and `mjpeg` URLs
1767
- * are constrained to their schemes, and it was missed the first time
1768
- * (Momus, W6 verification). The device string reaches
1725
+ * are constrained to their schemes. The device string reaches
1769
1726
  * `cv2.VideoCapture(device)` on the robot, and OpenCV does not restrict
1770
- * itself to devices: measured on cv2 4.5.4, an ordinary local video file
1771
- * opens and its pixels are published to the cloud, and so does
1772
- * `http://127.0.0.1:8899/secret.jpg`. Unconstrained, this field is an
1773
- * arbitrary local-file read *and* an outbound fetch from inside the robot
1774
- * — the §7.6 violation closed for the other two source kinds, reachable
1775
- * through the fourth, because "it is just a device path" read like a
1776
- * reason not to check.
1727
+ * itself to devices: an ordinary local video file opens and its pixels are
1728
+ * published to the cloud, and so does an `http://` URL pointing back inside
1729
+ * the robot's own network. Unconstrained, this field is an arbitrary
1730
+ * local-file read *and* an outbound fetch from inside the robot — the same
1731
+ * hole closed for the other two source kinds, reachable through the fourth,
1732
+ * because "it is just a device path" reads like a reason not to check.
1777
1733
  *
1778
1734
  * Narrower than the URL hole in one respect worth recording: a non-media
1779
1735
  * file and a missing file both fail to open, so this branch never worked
@@ -1909,7 +1865,7 @@ var configState = z5.object({
1909
1865
  applied_errors: z5.array(applyError).nullable()
1910
1866
  });
1911
1867
 
1912
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/introspection.js
1868
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/introspection.js
1913
1869
  import { z as z6 } from "zod";
1914
1870
  var rosGraphEntry = z6.object({
1915
1871
  name: rosName,
@@ -1948,7 +1904,7 @@ var typeDefinition = z6.discriminatedUnion("kind", [
1948
1904
  })
1949
1905
  ]);
1950
1906
 
1951
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/jobs.js
1907
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/jobs.js
1952
1908
  import { z as z7 } from "zod";
1953
1909
  var jobState = z7.enum(["running", "succeeded", "failed", "cancelled", "lost"]);
1954
1910
  var job = z7.object({
@@ -1969,17 +1925,17 @@ var job = z7.object({
1969
1925
  description: "When this job last changed, as an ISO 8601 timestamp."
1970
1926
  }),
1971
1927
  /**
1972
- * A monotonic counter, ascending in mint order (W7), and the **named**
1973
- * tiebreaker for any listing that claims an order.
1928
+ * A monotonic counter, ascending in mint order, and the **named** tiebreaker
1929
+ * for any listing that claims an order.
1974
1930
  *
1975
1931
  * `started_at` is not a total order: two jobs minted in the same millisecond
1976
1932
  * sort against each other arbitrarily, and arbitrarily means *differently on
1977
1933
  * each query* — so `GET /api/robots/:id/jobs`, which documents "newest
1978
- * first", can show one twice and the other not at all. Exactly the defect
1979
- * `auditEvent.seq` was added for in W6b, in a route the same wave shipped.
1934
+ * first", can show one twice and the other not at all. `auditEvent.seq`
1935
+ * exists for the same reason on the audit log.
1980
1936
  *
1981
1937
  * **Scoped honestly: per cloud process, per run.** Job state lives in memory
1982
- * (§6.1 — that is why `lost` exists at all), so this counter restarts when
1938
+ * — that is why `lost` exists at all — so this counter restarts when
1983
1939
  * the cloud does, alongside the jobs it orders. Sound, because it only ever
1984
1940
  * orders jobs that coexist in one registry — and stated, because a reader
1985
1941
  * who assumed `auditEvent.seq`'s durable semantics would be wrong.
@@ -1994,14 +1950,10 @@ var job = z7.object({
1994
1950
  * Present on `failed`; a human message, plus a code where one exists.
1995
1951
  *
1996
1952
  * `details` exists because a refusal that carries only prose forces every
1997
- * consumer to parse it. W6b shipped `job_queue_full` with a documented
1998
- * `{limit, queued}` payload and **nowhere to put it**: the bridge reports a
1999
- * full queue as a job error, this shape had no `details`, and so the numbers
2000
- * were formatted into the message and lost. The console then rendered a
2001
- * "wait for one of N to finish" alert from a shape nothing in the system
2002
- * produced, and its test built that shape by hand — three repos agreeing
2003
- * with each other about a payload none of them exchanged (Momus, W6b
2004
- * review).
1953
+ * consumer to parse it. A documented payload with nowhere to put it — the
1954
+ * bridge reports a full queue as a job error — ends up formatted into the
1955
+ * message and lost, and every consumer then builds the structured shape by
1956
+ * hand from its own assumption.
2005
1957
  *
2006
1958
  * Optional, because most job errors have nothing structured to add. Where a
2007
1959
  * code has a documented payload — `job_queue_full` has
@@ -2170,7 +2122,7 @@ var jobRunSummary = z7.object({
2170
2122
  since_ms: z7.number().int().nonnegative()
2171
2123
  });
2172
2124
 
2173
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/protocol.js
2125
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/protocol.js
2174
2126
  var MAX_PATIENCE_MS = 12e4;
2175
2127
  var MIN_PATIENCE_MS = 1e3;
2176
2128
  var activeJob = z8.object({
@@ -2184,7 +2136,7 @@ var bridgeHello = z8.object({
2184
2136
  token: z8.string().min(1),
2185
2137
  bridge_version: z8.string().min(1),
2186
2138
  /**
2187
- * Every job this bridge still knows about, right now (spec §6.1, W4).
2139
+ * Every job this bridge still knows about, right now.
2188
2140
  *
2189
2141
  * A reconnect and a restart look **identical** on the wire otherwise: same
2190
2142
  * token, same version, same frame. But they must end differently — after a
@@ -2199,16 +2151,9 @@ var bridgeHello = z8.object({
2199
2151
  * has none — which is exactly the truth the cloud needs. A breadcrumb file
2200
2152
  * would only add a window in which the crash beat the write.
2201
2153
  *
2202
- * Defaulted so pre-W4 bridges still parse; they had no jobs, so the empty
2203
- * list is also the correct answer for them.
2204
- *
2205
- * **Renamed from `active_job_ids` in W6b**, when the entries stopped being
2206
- * ids. A field called `_ids` holding objects is the shape this project has
2207
- * repeatedly been caught by — a name that describes what the field used to
2208
- * carry, kept because renaming looked like churn. Nothing is deployed yet
2209
- * (W8 is the first deployment), so the old name is gone rather than
2210
- * accepted alongside the new one: two accepted spellings would have to be
2211
- * supported and reconciled forever, and nobody is asking for that.
2154
+ * Defaulted, so a bridge that sends no such field still parses; a bridge
2155
+ * with no jobs and a bridge that does not report them both mean the cloud
2156
+ * has nothing to keep alive.
2212
2157
  */
2213
2158
  active_jobs: z8.array(activeJob).max(500).default([])
2214
2159
  });
@@ -2251,21 +2196,20 @@ var cloudInvoke = z8.object({
2251
2196
  job_id: z8.uuid(),
2252
2197
  slug,
2253
2198
  /**
2254
- * Already validated against §4.4 rules; the bridge validates structurally.
2199
+ * Already validated against the configuration's parameter rules; the bridge
2200
+ * validates structurally.
2255
2201
  *
2256
2202
  * **Flat, keyed by parameter name** — `{"target_x": 1}`. The key is a key of
2257
- * the entry's `parameters` mapping, not a path into the message. Those were
2258
- * the same thing until FL-002 and are now deliberately decoupled: a
2259
- * parameter keeps its name when the field it fills moves in the message
2260
- * tree, which is the same reason a slug is not a topic name.
2203
+ * the entry's `parameters` mapping, not a path into the message. The two are
2204
+ * deliberately decoupled: a parameter keeps its name when the field it fills
2205
+ * moves in the message tree, which is the same reason a slug is not a topic
2206
+ * name.
2261
2207
  *
2262
- * Three things follow, and the last one got stronger rather than weaker:
2263
- * the key a caller sends is the key a rule names, so a `parameter_invalid`
2264
- * reports something the caller can find; the console binds one input per
2265
- * parameter; and a position the template does not mark with `${…}` cannot
2266
- * be set by any caller at all. That last one used to be a rule about what
2267
- * no `parameterSpec` declared. It is now structural — the value has nowhere
2268
- * to go.
2208
+ * Three things follow: the key a caller sends is the key a rule names, so a
2209
+ * `parameter_invalid` reports something the caller can find; a UI binds one
2210
+ * input per parameter; and a position the template does not mark with
2211
+ * `${…}` cannot be set by any caller at all, structurally — the value has
2212
+ * nowhere to go.
2269
2213
  *
2270
2214
  * The bridge substitutes these values into the entry's `message` template
2271
2215
  * at its placeholder positions. It no longer unflattens a dotted path;
@@ -2273,18 +2217,17 @@ var cloudInvoke = z8.object({
2273
2217
  */
2274
2218
  params: z8.record(z8.string(), z8.unknown()),
2275
2219
  /**
2276
- * How long this one call is worth waiting for (W6b), already resolved by
2277
- * the cloud — the caller's `invokeRequest.patience_ms`, or
2220
+ * How long this one call is worth waiting for, already resolved by the
2221
+ * cloud — the caller's `invokeRequest.patience_ms`, or
2278
2222
  * `DEFAULT_PATIENCE_MS` when they named none.
2279
2223
  *
2280
2224
  * **Required here, optional at REST**, deliberately. At the REST edge an
2281
2225
  * absent value is a caller who did not care and gets the default. By the
2282
2226
  * time the frame is on this socket somebody has decided, and the bridge
2283
2227
  * must never be in the position of picking a number the cloud is already
2284
- * counting against — which is what two independent 15 s constants meant in
2285
- * practice: a bridge that gave up at 15.0 s and a cloud that gave up at
2286
- * 15.0 s, agreeing only by accident, with no way to tell whose deadline a
2287
- * caller had actually hit.
2228
+ * counting against. Two independent constants that happen to match give up
2229
+ * at the same moment by accident, with no way to tell whose deadline a
2230
+ * caller actually hit.
2288
2231
  */
2289
2232
  patience_ms: z8.number().int().min(MIN_PATIENCE_MS).max(MAX_PATIENCE_MS)
2290
2233
  });
@@ -2432,12 +2375,12 @@ var cloudCameraStart = z8.object({
2432
2375
  room: z8.string().min(1),
2433
2376
  token: z8.string().min(1),
2434
2377
  /**
2435
- * Names **this attempt** (W6b), and is echoed in the `camera_state` that
2436
- * answers it.
2378
+ * Names **this attempt**, and is echoed in the `camera_state` that answers
2379
+ * it.
2437
2380
  *
2438
- * W6a gave `camera_state` a `cause` and said in the same comment that a
2439
- * cause is not a correlation. This is the other half. Start a camera, have
2440
- * it fail slowly, start it again: the first attempt's failure arrives while
2381
+ * `camera_state.cause` says what kind of event a frame is; a cause is not a
2382
+ * correlation, and this is the other half. Start a camera, have it fail
2383
+ * slowly, start it again: the first attempt's failure arrives while
2441
2384
  * the second is in flight, matches on slug, and resolves the attempt it
2442
2385
  * knows nothing about. The viewer is then told the running stream failed,
2443
2386
  * for a reason belonging to an attempt that is already over.
@@ -2471,12 +2414,13 @@ var bridgeAssetProgress = z8.object({
2471
2414
  total: z8.number().int().nonnegative(),
2472
2415
  /**
2473
2416
  * **Each entry says why** — see `assetFailure` in `assets.ts` for the three
2474
- * kinds and why one word was not enough. The bound is `assets.ts`'s too: a
2475
- * `.dae` with 17,331 unresolvable internal references produced a frame 32
2476
- * bytes over `MAX_WS_PAYLOAD_BYTES`, and `ws` enforces that **before**
2477
- * delivery — so the outcome was the robot's own socket closed, mid-sync, by
2478
- * a file in its workspace (Kassandra-W7a). A producer at its own ceiling
2479
- * reports **one** `refused` entry naming the file, not one per reference.
2417
+ * kinds and why one word is not enough. The bound is `assets.ts`'s too: a
2418
+ * single `.dae` can carry tens of thousands of unresolvable internal
2419
+ * references, which is enough to push this frame past
2420
+ * `MAX_WS_PAYLOAD_BYTES`. That limit is enforced **before** delivery, so the
2421
+ * outcome is not a dropped frame but the robot's own socket closed mid-sync
2422
+ * by a file in its workspace. A producer at its own ceiling reports **one**
2423
+ * `refused` entry naming the file, not one per reference.
2480
2424
  */
2481
2425
  failed: z8.array(assetFailure).max(1e3),
2482
2426
  /**
@@ -2488,17 +2432,12 @@ var bridgeAssetProgress = z8.object({
2488
2432
  * answers with silence is a backstop nobody can debug, and the alternative
2489
2433
  * on the table was to report every requested URI in `failed`. That would
2490
2434
  * have made `failed` mean two different things at once — *could not be
2491
- * resolved* and *was never attempted* — which is the one-field-two-facts
2492
- * defect this project has now split five times (`set`/`readable`,
2493
- * `truncated`/`truncated_by`, `value`/`sample_count`, `publishing`/`cause`,
2494
- * and camera health's own).
2435
+ * resolved* and *was never attempted* — one field carrying two facts, each
2436
+ * overwriting the other.
2495
2437
  *
2496
2438
  * So: `running` while work is happening, `finished` when the bridge will
2497
2439
  * send no more for this sync, `refused_busy` when it never started because
2498
2440
  * another sync was in flight. `failed` keeps its single meaning.
2499
- *
2500
- * Raised by Rosie-W7, who found the gap by asking what a second request
2501
- * should do rather than picking the silent option.
2502
2441
  */
2503
2442
  state: z8.enum(["running", "finished", "refused_busy"])
2504
2443
  });
@@ -2514,15 +2453,13 @@ var bridgeCameraState = z8.object({
2514
2453
  publishing: z8.boolean(),
2515
2454
  error: z8.object({ code: z8.string().min(1), message: z8.string().min(1) }).nullable(),
2516
2455
  /**
2517
- * Why this frame was sent (W6a).
2456
+ * Why this frame was sent.
2518
2457
  *
2519
2458
  * Without it, `{publishing: false, error: null}` is sent for **three
2520
2459
  * different things** — an answer to `camera_stop`, a stream stopped by a
2521
- * configuration change, and a source that recovered — and the cloud can
2522
- * only tell them apart by remembering what it saw before. Deriving a cause
2523
- * from remembered state is precisely the inference this project keeps
2524
- * finding to be wrong, and W6a exists because four failures had been
2525
- * sharing one silence.
2460
+ * configuration change, and a source that recovered — and the cloud can only
2461
+ * tell them apart by remembering what it saw before. A cause derived from
2462
+ * remembered state is a guess.
2526
2463
  *
2527
2464
  * `'command'` this frame answers a `camera_start` / `camera_stop`.
2528
2465
  * `'source'` unsolicited: the source's own health changed, whether or
@@ -2532,29 +2469,24 @@ var bridgeCameraState = z8.object({
2532
2469
  * failure, and it must not be logged as one.
2533
2470
  * `'live_lost'` publishing ended unexpectedly after it had started.
2534
2471
  *
2535
- * Note it does **not** answer "which attempt is this?" — `camera_state`
2536
- * still has no request id, and that remains a named deferral in cluster C.
2537
- * `cause` says what kind of event this is; correlation is a separate fact
2538
- * and giving one field both jobs would be the same mistake again.
2472
+ * It does **not** answer "which attempt is this?" — `request_id` beside it
2473
+ * does. `cause` says what kind of event this is; correlation is a separate
2474
+ * fact, and giving one field both jobs would be the same mistake again.
2539
2475
  *
2540
- * Required, not optional: an absent cause would default to the reading
2541
- * somebody happens to assume, and every frame's sender knows its own
2542
- * reason. Old bridges fail validation on this frame — acceptable while
2543
- * nothing is deployed, and W8 is the first deployment.
2476
+ * Required, not optional: an absent cause would default to whatever reading
2477
+ * the receiver happens to assume, and every frame's sender knows its own
2478
+ * reason.
2544
2479
  */
2545
2480
  cause: z8.enum(["command", "source", "config_change", "live_lost"]),
2546
2481
  /**
2547
2482
  * When the **robot** observed this state — bridge capture time, never
2548
- * receive time, the same discipline `timestamp_ms` follows for samples
2549
- * (spec §6.3).
2483
+ * receive time, the same discipline `timestamp_ms` follows for samples.
2550
2484
  *
2551
- * It exists because the cloud stamped `resourceHealthState.changed_at_ms`
2552
- * with its own `Date.now()`, and a **restatement** is by definition an old
2553
- * state re-sent into an empty map. So after a cloud restart every failure —
2554
- * including one from yesterday — was dated to the restart, in the one
2555
- * scenario `changed_at_ms`'s own doc comment was written for: *"a page that
2556
- * loads late must be able to tell a failure from a minute ago from one from
2557
- * yesterday"*.
2485
+ * It exists because a cloud that stamps `resourceHealthState.changed_at_ms`
2486
+ * with its own clock dates every **restatement** to the moment it restarted
2487
+ * — a restatement is by definition an old state re-sent into an empty map.
2488
+ * That destroys exactly what `changed_at_ms` is for: a page that loads late
2489
+ * must be able to tell a failure from a minute ago from one from yesterday.
2558
2490
  *
2559
2491
  * On a restatement this carries **when the state was first observed**, not
2560
2492
  * when the frame was sent. A bridge that re-states a failure it has held for
@@ -2562,7 +2494,7 @@ var bridgeCameraState = z8.object({
2562
2494
  */
2563
2495
  observed_at_ms: z8.number().int().nonnegative(),
2564
2496
  /**
2565
- * Which request this frame answers (W6b), or `null` when it answers none.
2497
+ * Which request this frame answers, or `null` when it answers none.
2566
2498
  *
2567
2499
  * `null` is not a gap and must not be treated as one: a `cause: 'source'`
2568
2500
  * frame — the unsolicited health report that makes a wrong password visible
@@ -2579,21 +2511,21 @@ var bridgeCameraState = z8.object({
2579
2511
  * **The pairing rule is not in this schema, deliberately.** "Non-null iff
2580
2512
  * `cause === 'command'`" is a cross-field constraint; a zod `.refine()`
2581
2513
  * would express it at runtime and then **disappear** from the generated
2582
- * JSON Schema, which is what the bridge vendors. The cloud would reject
2583
- * frames the bridge had validated as correct — the same artifact/runtime
2584
- * divergence that `.default()` publishing as `required` has produced four
2585
- * times in this project, only pointing the other way. The rule is enforced
2514
+ * JSON Schema, which is what a non-TypeScript bridge validates against. The
2515
+ * cloud would reject frames the bridge had validated as correct — the same
2516
+ * artifact-versus-runtime divergence that `.default()` publishing as
2517
+ * `required` produces, pointing the other way. The rule is enforced
2586
2518
  * where the correlation is used, in the cloud's bridge frame handler, and
2587
2519
  * stated here so nobody has to derive it from that code.
2588
2520
  */
2589
2521
  request_id: z8.string().min(1).max(64).nullable()
2590
2522
  });
2591
2523
 
2592
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/config-issues.js
2524
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/config-issues.js
2593
2525
  var EXPOSURE_SECTIONS = ["datapoints", "actions", "services", "publishers", "cameras"];
2594
2526
  var EXPOSURE_SECTION_NAMES = new Set(EXPOSURE_SECTIONS);
2595
2527
 
2596
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/rest.js
2528
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/rest.js
2597
2529
  import { z as z9 } from "zod";
2598
2530
  var robot = z9.object({
2599
2531
  id: z9.uuid().meta({
@@ -2733,7 +2665,7 @@ var invokeRequest = z9.object({
2733
2665
  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."
2734
2666
  }),
2735
2667
  /**
2736
- * How long **this call** is worth waiting for, in milliseconds (W6b).
2668
+ * How long **this call** is worth waiting for, in milliseconds.
2737
2669
  *
2738
2670
  * **Absent means `DEFAULT_PATIENCE_MS`** — today's behaviour, unchanged, for
2739
2671
  * every caller who does not care. It is optional because most callers have
@@ -2848,16 +2780,14 @@ var cameraListResponse = z9.object({
2848
2780
  });
2849
2781
  var liveSessionResponse = z9.object({
2850
2782
  /**
2851
- * This viewer's hold, and the **only** thing `DELETE` should be given
2852
- * (W6b).
2783
+ * This viewer's hold, and the **only** thing `DELETE` should be given.
2853
2784
  *
2854
- * A hold was addressed by `{identity, robot, slug}` and nothing else, so
2855
- * two tabs of one logged-in user were one hold as far as the refcount could
2856
- * see. Closing either tab released it: the second tab kept its LiveKit
2857
- * connection — the token is checked at join and never again — and went on
2858
- * rendering a video that the robot had already stopped producing. The
2859
- * viewer sees a frozen picture, not an ended session, which is the failure
2860
- * this project rejects everywhere else.
2785
+ * A hold addressed by `{identity, robot, slug}` alone would make two tabs of
2786
+ * one logged-in user a single hold as far as the refcount can see. Closing
2787
+ * either tab would release it, and the surviving tab would keep its LiveKit
2788
+ * connection — the token is checked at join and never again — rendering a
2789
+ * video the robot had already stopped producing. A frozen picture is not an
2790
+ * ended session.
2861
2791
  *
2862
2792
  * `DELETE` without a session id keeps today's meaning — *release my holds
2863
2793
  * on this camera* — because an SDK that has lost its id, or a client that
@@ -2915,26 +2845,23 @@ var historyQuery = z9.object({
2915
2845
  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."
2916
2846
  }),
2917
2847
  /**
2918
- * **A union whose input branch IS the wire, not a coercion (W9d, DEF-059).**
2848
+ * **A union whose input branch IS the wire, not a coercion.**
2919
2849
  *
2920
- * This was `z.coerce.number()`, for a good reason that stayed true: the
2921
- * schema describes a **query string**, where every value arrives as text,
2922
- * and a bare `z.number()` would make each route coerce by hand. What was
2923
- * measured afterwards is that a coercion cannot be *published*: zod renders
2924
- * a coercion's **result** in either `io` mode, so `io: 'input'` and
2925
- * `io: 'output'` both emit `{"type":"integer"}` — an artifact describing a
2850
+ * The schema describes a **query string**, where every value arrives as
2851
+ * text. `z.coerce.number()` would read it, but a coercion cannot be
2852
+ * *published*: zod renders a coercion's **result** in either `io` mode, so
2853
+ * input and output both emit `{"type":"integer"}` — an artifact describing a
2926
2854
  * shape a query string can never carry. Anyone validating a real request
2927
2855
  * against it rejects every one that sets `limit`.
2928
2856
  *
2929
- * That is a **different** defect from the `.default()` class, which
2930
- * `io: 'input'` genuinely does fix; `export-schemas.ts` once claimed one
2931
- * remedy for both and has been corrected.
2857
+ * That is a different problem from `.default()` publishing as required,
2858
+ * which input-mode export genuinely does fix.
2932
2859
  *
2933
- * A union states both truths honestly: the wire carries a numeric string,
2934
- * a programmatic caller may pass a number, and the artifact can render the
2860
+ * A union states both truths honestly: the wire carries a numeric string, a
2861
+ * programmatic caller may pass a number, and the artifact can render the
2935
2862
  * input branch because there is one to render.
2936
2863
  *
2937
- * **What the artifact no longer says, named here rather than left silent.**
2864
+ * **What the artifact does not say, named here rather than left silent.**
2938
2865
  * The `1..10000` bound lives in the `.pipe()`, which is the *output* half, so
2939
2866
  * no input-mode artifact can express it as a constraint: the published shape
2940
2867
  * is `^\d{1,5}$` or a bare integer, and five digits is a weak echo of the
@@ -2942,11 +2869,10 @@ var historyQuery = z9.object({
2942
2869
  * parsing, not by the shape of the text — but it is a **reduction**, and an
2943
2870
  * artifact that stops naming a bound reads as if there were none.
2944
2871
  *
2945
- * So both branches carry the number in a `.describe()` (Nimbus-W9d's
2946
- * proposal). It is **not** a constraint and nothing validates against it; it
2947
- * means a generator, or a person reading only the published schema, sees the
2948
- * actual ceiling instead of nothing. The gap is narrowed and named rather
2949
- * than closed.
2872
+ * So both branches carry the number in a `.describe()`. It is **not** a
2873
+ * constraint and nothing validates against it; it means a generator, or a
2874
+ * person reading only the published schema, sees the actual ceiling instead
2875
+ * of nothing. The gap is narrowed and named rather than closed.
2950
2876
  */
2951
2877
  limit: z9.union([
2952
2878
  z9.string().regex(/^\d{1,5}$/).describe("Positive integer, 1-10000. The pattern only bounds digit count; the real ceiling is enforced after parsing."),
@@ -3067,8 +2993,7 @@ var robotDeletionSummary = z9.object({
3067
2993
  * console renders them in one sentence: *"this deletes N published slugs …
3068
2994
  * and M cameras"*. With cameras inside `slug_count` that sentence counts
3069
2995
  * them twice, on the one screen whose whole justification is naming what an
3070
- * irreversible click destroys (Momus, W6a review — the cloud summed all
3071
- * five and the console then added the cameras again).
2996
+ * irreversible click destroys.
3072
2997
  *
3073
2998
  * A draft is destroyed too and is described by `had_unpublished_draft`
3074
2999
  * rather than by either of these: describing three things with two numbers
@@ -3079,7 +3004,7 @@ var robotDeletionSummary = z9.object({
3079
3004
  bytes_freed: z9.number().int().nonnegative(),
3080
3005
  cameras: z9.array(slug),
3081
3006
  /**
3082
- * Assets destroyed with the robot (W7), and **`asset_bytes_freed` is what
3007
+ * Assets destroyed with the robot, and **`asset_bytes_freed` is what
3083
3008
  * this org actually gets back** — not the sum of the assets' sizes.
3084
3009
  *
3085
3010
  * Storage is content-addressed, so a mesh two robots share survives the
@@ -3153,7 +3078,7 @@ var RESOURCE_HEALTH_STATES = [
3153
3078
  "unreadable_credential",
3154
3079
  /**
3155
3080
  * A camera names a credential that **does not exist** in this org — deleted,
3156
- * mistyped, or belonging to somebody else (W6a review).
3081
+ * mistyped, or belonging to somebody else.
3157
3082
  *
3158
3083
  * Separate from `unreadable_credential` because that one asserts a
3159
3084
  * decryption that was attempted and failed, and here nothing was ever
@@ -3163,11 +3088,11 @@ var RESOURCE_HEALTH_STATES = [
3163
3088
  * — a different fact with a different fix.
3164
3089
  *
3165
3090
  * **Retiring with the credential store**, and not live behaviour to build
3166
- * against. Its one producer was `cloud-config-frame.ts` tolerating an
3167
- * unresolved `credentials_ref` at publish time; FL-002 deleted that field,
3168
- * so nothing emits this today. It is kept only until the wave that removes
3169
- * the store also removes these three credential states — `unreadable_credential`
3170
- * and the `readable` fact on `credentialSummary` go the same way.
3091
+ * against. Its one producer tolerated an unresolved `credentials_ref` at
3092
+ * publish time; that field is gone, so nothing emits this today. It is kept
3093
+ * only until the credential store is removed, which takes these three
3094
+ * credential states with it — `unreadable_credential` and the `readable` fact
3095
+ * on `credentialSummary` go the same way.
3171
3096
  */
3172
3097
  "credential_missing",
3173
3098
  /** A configuration change stopped this stream, deliberately. */
@@ -3192,18 +3117,17 @@ var resourceHealthState = z9.object({
3192
3117
  /** The camera slug, or the credential name. */
3193
3118
  ref: z9.string().min(1).max(64),
3194
3119
  /**
3195
- * **Which of two questions this entry answers (W9a, DEF-072).**
3120
+ * **Which of two questions this entry answers.**
3196
3121
  *
3197
3122
  * `'source'` — can the source be read at all? (`unreachable`, `auth_failed`,
3198
3123
  * `unreadable_credential`, `missing_credential`, `ok`, …)
3199
3124
  * `'publish'` — given a readable source, did publishing to LiveKit work?
3200
3125
  *
3201
- * Before this, both went into one entry keyed `${robot} ${kind} ${ref}` with
3202
- * one flat `state`, in which `publish_failed` answered *"can we publish"*
3203
- * and every other value answered *"can the source be read"* — **same key,
3204
- * same field, two questions**, so each overwrote the other. The conflation
3205
- * was once an occasional race; W6a's reconnect restatement made it
3206
- * guaranteed, on every reconnect, for any camera with an active viewer.
3126
+ * Without the facet both answers land in one entry keyed
3127
+ * `${robot} ${kind} ${ref}` with one flat `state`, in which `publish_failed`
3128
+ * answers *"can we publish"* and every other value answers *"can the source
3129
+ * be read"* — same key, same field, two questions, each overwriting the
3130
+ * other.
3207
3131
  *
3208
3132
  * The facet is part of the entry's identity: a camera can perfectly well be
3209
3133
  * readable and unpublishable at the same moment, and that pair is exactly
@@ -3236,30 +3160,28 @@ var orgQuotas = z9.object({
3236
3160
  max_retention_writes_per_minute: z9.number().int().nonnegative(),
3237
3161
  max_realtime_connections: z9.number().int().positive(),
3238
3162
  /**
3239
- * Asset storage (§4.6, W7) — **its own dial, not part of
3240
- * `max_retention_bytes`.** A sync grows storage in jumps and time series
3241
- * grow steadily; one dial would let the first crowd out the second, and the
3242
- * org that hit its limit would be told to look at the wrong thing.
3163
+ * Asset storage — **its own dial, not part of `max_retention_bytes`.** A
3164
+ * sync grows storage in jumps and time series grow steadily; one dial would
3165
+ * let the first crowd out the second, and the org that hit its limit would be
3166
+ * told to look at the wrong thing.
3243
3167
  *
3244
3168
  * **Counted per distinct blob *this org references* — not per asset row, and
3245
- * not per object the platform stores on its behalf (W7a, D1).** The two
3246
- * readings are indistinguishable from the number alone and a customer is
3247
- * entitled to know which one they are being charged for.
3169
+ * not per object the platform stores on its behalf.** The two readings are
3170
+ * indistinguishable from the number alone and a customer is entitled to know
3171
+ * which one they are being charged for.
3248
3172
  *
3249
3173
  * Within an org, sharing is free: two robots referencing the same mesh cost
3250
3174
  * one copy, which is what dedup means to a customer, and anything else
3251
3175
  * charges an org twice for a fleet of identical robots — the normal case.
3252
3176
  *
3253
- * **Across orgs, sharing is not free, and W7 shipped the opposite.** Storage
3254
- * stays globally content-addressed (one object per sha256; that efficiency
3255
- * is real), but accounting is per-org: an org is charged for each distinct
3256
- * blob it references and credited when its own last reference goes, whether
3257
- * or not the blob survives for somebody else. Global refcounting made the
3258
- * first org to sync a blob pay for it forever while every later org stored
3259
- * it free — so the quota was evadable by anyone whose mesh someone else had
3260
- * already uploaded, and an org's own number depended on who got there first,
3261
- * which nobody can predict. Measured before the change: 342 bytes held by an
3262
- * org owning no assets, with no operation able to free them.
3177
+ * **Across orgs, sharing is not free.** Storage stays globally
3178
+ * content-addressed (one object per sha256; that efficiency is real), but
3179
+ * accounting is per-org: an org is charged for each distinct blob it
3180
+ * references and credited when its own last reference goes, whether or not
3181
+ * the blob survives for somebody else. Global refcounting would make the
3182
+ * first org to sync a blob pay for it forever while every later org stored it
3183
+ * free — a quota evadable by anyone whose mesh someone else had already
3184
+ * uploaded, and an org's own number would depend on who got there first.
3263
3185
  */
3264
3186
  max_asset_storage_bytes: z9.number().int().nonnegative()
3265
3187
  });
@@ -3303,7 +3225,7 @@ var robotLatencySeries = z9.object({
3303
3225
  });
3304
3226
  var orgLatencyQuery = z9.object({
3305
3227
  from_ms: wireTimestampMs,
3306
- /** Exclusive — half-open `[from, to)`, the convention every other query here already follows (DEF-062). */
3228
+ /** Exclusive — half-open `[from, to)`, the convention every other query here already follows. */
3307
3229
  to_ms: wireTimestampMs,
3308
3230
  /**
3309
3231
  * One robot's own sparkline. `z.uuid()`, because the column is one —
@@ -3363,13 +3285,13 @@ var slugUsageResponse = z9.object({
3363
3285
  alert_count: z9.number().int().nonnegative()
3364
3286
  });
3365
3287
 
3366
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/realtime.js
3288
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/realtime.js
3367
3289
  import { z as z14 } from "zod";
3368
3290
 
3369
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/client-auth.js
3291
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/client-auth.js
3370
3292
  import { z as z13 } from "zod";
3371
3293
 
3372
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/apps.js
3294
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/apps.js
3373
3295
  import { z as z10 } from "zod";
3374
3296
  var appIdentifier = slug;
3375
3297
  var app = z10.object({
@@ -3385,7 +3307,7 @@ var app = z10.object({
3385
3307
  identifier: appIdentifier.meta({
3386
3308
  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`."
3387
3309
  }),
3388
- /** Robots are referenced individually; tags never grant rights (§12.2). */
3310
+ /** Robots are referenced individually; tags never grant rights. */
3389
3311
  robot_ids: z10.array(z10.uuid()).meta({
3390
3312
  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."
3391
3313
  }),
@@ -3438,16 +3360,17 @@ var updateAppRequest = z10.object({
3438
3360
  name: z10.string().min(1).max(120).optional(),
3439
3361
  robot_ids: z10.array(z10.uuid()).optional(),
3440
3362
  /**
3441
- * `app.default_role_id`'s write half — an app *setting*, which is where D1
3442
- * put the default role, so it belongs on the app's own PATCH and not on a
3443
- * route of its own.
3444
- *
3445
- * **`.nullable().optional()`, and the two mean different things.** Absent
3446
- * leaves the current default alone; an explicit `null` clears it. A field
3447
- * that could only be set and never unset would make "we changed our mind"
3448
- * unreachable through the API — the same silence `.strict()` above exists to
3449
- * avoid, from the other direction.
3450
- */
3363
+ * `app.default_role_id`'s write half — an app *setting*, which is where the
3364
+ * two-identity-space model
3365
+ * put the default role, so it belongs on the app's own PATCH and not on a
3366
+ * route of its own.
3367
+ *
3368
+ * **`.nullable().optional()`, and the two mean different things.** Absent
3369
+ * leaves the current default alone; an explicit `null` clears it. A field
3370
+ * that could only be set and never unset would make "we changed our mind"
3371
+ * unreachable through the API — the same silence `.strict()` above exists to
3372
+ * avoid, from the other direction.
3373
+ */
3451
3374
  default_role_id: z10.uuid().nullable().optional()
3452
3375
  }).strict();
3453
3376
  var serverKeyToken = z10.string().regex(/^flk_[0-9a-f]{32}$/);
@@ -3500,16 +3423,13 @@ var roleListResponse = z10.object({
3500
3423
  var rolePermissions = z10.object({
3501
3424
  role_id: z10.uuid(),
3502
3425
  /**
3503
- * **A slug is unique per robot across ALL service kinds** (spec §4.1:
3504
- * "Jeder Dienst erhält einen Slug" — one namespace, not one per kind), and
3505
- * the cloud's config validation enforces that with a kind-agnostic
3506
- * collection pass. That is why this list carries slugs and not
3507
- * (kind, slug) pairs: when W4 adds actions, services and publishers, a
3508
- * grant keeps meaning exactly what it means today, and this shape does not
3509
- * change. What W4 does need is an endpoint that lists every *grantable*
3510
- * slug of a robot with its kind, so the console's matrix can offer them —
3511
- * today it enumerates datapoints only, which is the seam that would
3512
- * otherwise force a rebuild.
3426
+ * **A slug is unique per robot across ALL exposure kinds** — one namespace,
3427
+ * not one per kind — and the cloud's configuration validation enforces that
3428
+ * with a kind-agnostic collection pass. That is why this list carries slugs
3429
+ * and not (kind, slug) pairs: a grant means the same thing whichever kind the
3430
+ * slug turns out to name. `GET /api/robots/:id/exposures` is the companion
3431
+ * read that lists every grantable slug of a robot **with** its kind, so a
3432
+ * rights matrix can offer them.
3513
3433
  */
3514
3434
  grants: z10.array(z10.object({
3515
3435
  robot_id: z10.uuid(),
@@ -3518,25 +3438,22 @@ var rolePermissions = z10.object({
3518
3438
  /**
3519
3439
  * App-wide abilities a role grants, as opposed to per-slug grants above.
3520
3440
  *
3521
- * **A capability here is a promise, and one of them is still not kept.**
3522
- * `action_history` and `presence` were both gated by this object from W4
3523
- * and implemented nowhere — no route, no SDK method, no realtime frame
3524
- * (register row 8). A console could therefore switch them on and nothing
3525
- * changed, which is worse than their absence: the developer believes they
3526
- * granted something. **This paragraph stays** whatever the current tally
3527
- * is: it is the only place that says a switch in the console may change
3528
- * nothing, and it is how the next unkept capability gets caught.
3441
+ * **A capability here is a promise, and one of them is still not kept.** A
3442
+ * capability with no route, no SDK method and no realtime frame behind it can
3443
+ * be switched on while nothing changes, which is worse than its absence: the
3444
+ * developer believes they granted something. **This paragraph stays**
3445
+ * whatever the current tally is: it is the only place that says a switch may
3446
+ * change nothing, and it is how the next unkept capability gets caught.
3529
3447
  *
3530
- * `assets` (W7) was the first one redeemed. It gates §4.6's asset store,
3531
- * which is not covered by `grants` because **assets are not slugs** — and it
3532
- * is its own decision rather than a side effect of reaching the robot,
3533
- * because a mesh set gives away the machine's build.
3448
+ * `assets` gates the asset store, which is not covered by `grants` because
3449
+ * **assets are not slugs** — and it is its own decision rather than a side
3450
+ * effect of reaching the robot, because a mesh set gives away the machine's
3451
+ * build.
3534
3452
  *
3535
- * **`action_history` is kept as of the run-history delta.** It gates
3453
+ * **`action_history` is kept.** It gates
3536
3454
  * `GET /api/robots/:id/jobs/history` — an end user whose role lacks it is
3537
3455
  * refused `403 capability_required`, naming the capability so the developer
3538
- * knows which switch is off. It was unkeepable while nothing durable
3539
- * recorded what had run; `jobRun` and `job_runs` are that record.
3456
+ * knows which switch is off.
3540
3457
  *
3541
3458
  * **What granting it discloses.** A `jobRun` names the actor who invoked
3542
3459
  * it, and `jobActor.label` is an email — so an end user holding this
@@ -3556,10 +3473,10 @@ var rolePermissions = z10.object({
3556
3473
  })
3557
3474
  });
3558
3475
 
3559
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/app-users.js
3476
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/app-users.js
3560
3477
  import { z as z12 } from "zod";
3561
3478
 
3562
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/identity.js
3479
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/identity.js
3563
3480
  import { z as z11 } from "zod";
3564
3481
  var password = z11.string().min(12).max(256);
3565
3482
  var USER_DISPLAY_NAME_MAX = 120;
@@ -3721,7 +3638,7 @@ var authMeResponse = z11.object({ org, user: fleetlessUser });
3721
3638
  var patchOrgRequest = z11.object({ name: z11.string().min(1).max(120) }).strict();
3722
3639
  var patchAuthMeRequest = z11.object({ display_name: z11.string().min(1).max(USER_DISPLAY_NAME_MAX).nullable() }).strict();
3723
3640
 
3724
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/app-users.js
3641
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/app-users.js
3725
3642
  var APP_USER_DISPLAY_NAME_MAX = 120;
3726
3643
  var providerSlug = z12.string().max(40).regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, "a provider slug is lowercase and hyphen-separated, starting with a letter");
3727
3644
  var appUserStatus = z12.enum(["pending_verification", "active", "blocked"]);
@@ -3870,7 +3787,7 @@ var createAppOidcProviderRequest = z12.object({
3870
3787
  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."
3871
3788
  }),
3872
3789
  scopes: z12.array(z12.string().min(1).max(60)).min(1).max(20).default(["openid", "email", "profile"]).meta({
3873
- 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."
3790
+ 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."
3874
3791
  }),
3875
3792
  link_verified_emails: z12.boolean().default(false).meta({
3876
3793
  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."
@@ -3972,7 +3889,7 @@ var mailOutcome = z12.object({
3972
3889
  })
3973
3890
  });
3974
3891
 
3975
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/client-auth.js
3892
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/client-auth.js
3976
3893
  var clientLoginRequest = z13.object({
3977
3894
  app_identifier: appIdentifier.meta({
3978
3895
  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."
@@ -4110,7 +4027,7 @@ var clientMcpInteraction = z13.object({
4110
4027
  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."
4111
4028
  }),
4112
4029
  scopes: z13.array(z13.string()).meta({ description: "The scopes the client asked for, to show the person before they approve." }),
4113
- already_granted: z13.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." }),
4030
+ already_granted: z13.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." }),
4114
4031
  expires_at: z13.iso.datetime().meta({ description: "When the interaction stops being approvable. Ten minutes from the authorize step; afterwards both approve and deny answer `interaction_expired`." })
4115
4032
  });
4116
4033
  var clientMcpInteractionDecisionResponse = z13.object({
@@ -4161,7 +4078,7 @@ var clientIdentity = z13.object({
4161
4078
  })
4162
4079
  });
4163
4080
 
4164
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/realtime.js
4081
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/realtime.js
4165
4082
  var clientAuth = z14.object({
4166
4083
  type: z14.literal("auth"),
4167
4084
  token: z14.string().min(1)
@@ -4180,20 +4097,16 @@ var clientInvoke = z14.object({
4180
4097
  request_id: z14.string().min(1).max(64),
4181
4098
  robot_id: z14.uuid(),
4182
4099
  slug,
4183
- /** Parameters by field path, validated against the config's rules (§4.4). */
4100
+ /** Parameters by field path, validated against the configuration's rules. */
4184
4101
  params: z14.record(z14.string(), z14.unknown()),
4185
4102
  /**
4186
- * How long this one call is worth waiting for (W6b) — the same field,
4187
- * meaning and cap as `invokeRequest.patience_ms`; absent means
4188
- * `DEFAULT_PATIENCE_MS`.
4103
+ * How long this one call is worth waiting for — the same field, meaning and
4104
+ * cap as `invokeRequest.patience_ms`; absent means `DEFAULT_PATIENCE_MS`.
4189
4105
  *
4190
- * It is here because **§11.1 parity is a rule, not a preference**: what REST
4191
- * can do travels over this socket. The first version of this delta gave
4192
- * `patience_ms` to the REST body only — and the SDK invokes exclusively over
4193
- * the realtime channel, so the field would have been unreachable for every
4194
- * SDK caller while appearing in the documentation. W6a shipped four SDK
4195
- * methods no SDK caller could invoke; this is the same defect caught before
4196
- * it shipped, by the SDK owner rather than by a reviewer.
4106
+ * It is here because **parity is a rule, not a preference**: what REST can do
4107
+ * travels over this socket. A field given to the REST body alone would be
4108
+ * unreachable to every caller that invokes over the realtime channel, while
4109
+ * still appearing in the documentation.
4197
4110
  */
4198
4111
  patience_ms: z14.number().int().min(MIN_PATIENCE_MS).max(MAX_PATIENCE_MS).optional()
4199
4112
  });
@@ -4201,17 +4114,17 @@ var clientCancel = z14.object({
4201
4114
  type: z14.literal("cancel"),
4202
4115
  request_id: z14.string().min(1).max(64),
4203
4116
  robot_id: z14.uuid(),
4204
- /** Which slug — required, and the only address a cancel had until W6b. */
4117
+ /** Which slug — required, and the coarse address of a cancel. */
4205
4118
  slug,
4206
4119
  /**
4207
- * Which job on that slug (W6b), or `null` for *whatever is running there*.
4120
+ * Which job on that slug, or `null` for *whatever is running there*.
4208
4121
  *
4209
4122
  * The two are different requests and both are legitimate. An operator
4210
4123
  * hitting a stop button means the second: stop the machine, whatever it is
4211
4124
  * doing. A client cancelling the job it started means the first — and until
4212
- * this field existed it could not say so, so a cancel that arrived just
4213
- * after its own job ended stopped the next caller's job instead. Same slug,
4214
- * same wire frame, entirely different machine behaviour, and nothing in the
4125
+ * this field a client could not say so, and a cancel arriving just after its
4126
+ * own job ended would stop the next caller's job instead. Same slug, same
4127
+ * wire frame, entirely different machine behaviour, and nothing in the
4215
4128
  * protocol able to tell them apart.
4216
4129
  *
4217
4130
  * A named id that is not running answers `not_found` rather than falling
@@ -4237,9 +4150,9 @@ var commandResult = z14.object({
4237
4150
  * - `ok:true` on an invoke or a call: the job that was just created.
4238
4151
  * - `ok:true` on a cancel: the job the cancel was sent to.
4239
4152
  * - `ok:false, code:'busy'`: **the job that is already running** — the
4240
- * caller has none. This is the §11.3 "inkl. Information, was läuft", and
4241
- * it is the whole reason a busy refusal is useful: the caller learns
4242
- * whether to wait or to give up (see `busyDetails`).
4153
+ * caller has none. Naming what is already running is the whole reason a
4154
+ * busy refusal is useful: the caller learns whether to wait or to give up
4155
+ * (see `busyDetails`).
4243
4156
  * - any other refusal: `null`.
4244
4157
  */
4245
4158
  job: job.nullable(),
@@ -4261,12 +4174,11 @@ var commandResult = z14.object({
4261
4174
  * The same payload the REST envelope carries in `apiError.details` — for
4262
4175
  * `parameter_invalid`, a `parameterInvalidDetails`.
4263
4176
  *
4264
- * Added because it was missing, and its absence quietly broke §11.1: this
4265
- * socket is supposed to do *everything* REST can do, but a
4266
- * `parameter_invalid` arriving here had nowhere to put its violations, so
4267
- * the same refusal was actionable over HTTP and opaque over the socket.
4268
- * A client cannot bind an error to the input that caused it from a code
4269
- * alone — which is the entire point of the flat parameter shape.
4177
+ * It is here because this socket does *everything* REST can do, and without
4178
+ * it a `parameter_invalid` arriving here would have nowhere to put its
4179
+ * violations — the same refusal actionable over HTTP and opaque over the
4180
+ * socket. A client cannot bind an error to the input that caused it from a
4181
+ * code alone, which is the entire point of the flat parameter shape.
4270
4182
  */
4271
4183
  details: z14.unknown().optional()
4272
4184
  });
@@ -4280,7 +4192,7 @@ var clientSubscribe = z14.object({
4280
4192
  robot_id: z14.uuid(),
4281
4193
  slug,
4282
4194
  /**
4283
- * What the subscriber expects, and how it wants it (W5).
4195
+ * What the subscriber expects, and how it wants it.
4284
4196
  *
4285
4197
  * `kind` lets the server answer **`wrong_kind`** instead of accepting a
4286
4198
  * subscribe the client will then filter to silence — and silence is
@@ -4288,8 +4200,6 @@ var clientSubscribe = z14.object({
4288
4200
  * Optional, so an older client that omits it keeps today's behaviour.
4289
4201
  *
4290
4202
  * `options` is where a camera says what it wants; a datapoint needs none.
4291
- * It exists now rather than later because adding a field to a frame three
4292
- * repos parse is cheap once and expensive twice.
4293
4203
  */
4294
4204
  /**
4295
4205
  * `publisher` is here even though a publisher is not subscribable: a client
@@ -4339,7 +4249,7 @@ var liveSessionEndReason = z14.enum([
4339
4249
  /**
4340
4250
  * The cloud ended it and cannot say which of the above applied. **Kept
4341
4251
  * deliberately**: a channel that cannot say "I do not know" will say
4342
- * something false instead, and this project has paid for that four times in
4252
+ * something false instead, which is the costlier failure in
4343
4253
  * the camera path alone.
4344
4254
  */
4345
4255
  "unknown"
@@ -4354,13 +4264,10 @@ var liveSessionEvent = z14.object({
4354
4264
  /**
4355
4265
  * **Classified text the cloud produced, never text the robot sent.**
4356
4266
  *
4357
- * An earlier draft of this comment said *"the robot's own words when it has
4358
- * any"*, which reads as permission to pass `bridgeCameraState.error.message`
4359
- * straight through. Nothing sanitises that field, and this codebase has a
4360
- * documented incident of a password reaching a developer surface through
4361
- * exactly that route — `camera-health.ts`'s fixed-string `REASON` discipline
4362
- * exists because of it. Nimbus-W9a stopped at the sentence and asked rather
4363
- * than taking the permission it appeared to give (2026-08-19).
4267
+ * It is **not** the robot's own words. Nothing sanitises
4268
+ * `bridgeCameraState.error.message`, and a camera password reaches a
4269
+ * developer surface through exactly that route — which is why the cloud maps
4270
+ * a robot's diagnosis to fixed strings rather than forwarding it.
4364
4271
  *
4365
4272
  * So: `null` unless the cloud itself has something classified to say. If a
4366
4273
  * developer needs the robot's own diagnosis later, it arrives as a mapped
@@ -4449,7 +4356,7 @@ var orgEventDropped = z14.object({
4449
4356
  dropped: z14.number().int().positive()
4450
4357
  }).strict();
4451
4358
 
4452
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/audit.js
4359
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/audit.js
4453
4360
  import { z as z15 } from "zod";
4454
4361
  var auditActor = z15.object({
4455
4362
  kind: z15.enum(["developer", "end_user", "app_user", "server_key", "bridge"]),
@@ -4461,8 +4368,7 @@ var auditEvent = z15.object({
4461
4368
  org_id: z15.uuid(),
4462
4369
  at: z15.iso.datetime(),
4463
4370
  /**
4464
- * A monotonic counter, ascending in write order, unique across the log
4465
- * (W6b).
4371
+ * A monotonic counter, ascending in write order, unique across the log.
4466
4372
  *
4467
4373
  * `at` is not a total order. Two events written in the same millisecond —
4468
4374
  * a login and the config publish it enables, a cascade writing several
@@ -4474,11 +4380,7 @@ var auditEvent = z15.object({
4474
4380
  *
4475
4381
  * It is also the only correct **cursor** for paging this log, for the same
4476
4382
  * reason: a cursor that is not unique either skips rows or repeats them at
4477
- * every page boundary. No cursor parameter exists on `GET /api/audit` yet —
4478
- * the route returns the whole log — and that is stated here rather than
4479
- * implied, because a contract that describes a capability the API does not
4480
- * have is the defect this project keeps finding. When paging is added it
4481
- * uses this field; nothing else in this shape can carry it.
4383
+ * every page boundary. Nothing else in this shape can carry one.
4482
4384
  *
4483
4385
  * Required, not optional: an event without a sequence cannot be ordered
4484
4386
  * against one that has it, and a log with two orderings has none.
@@ -4515,7 +4417,7 @@ var auditQuery = z15.object({
4515
4417
  /** Only events with a smaller `seq` — the next, older page. */
4516
4418
  before_seq: wireSeqCursor.optional(),
4517
4419
  /**
4518
- * Same shape as DEF-059's `historyQuery.limit`: a union whose input branch
4420
+ * The same shape as `historyQuery.limit`: a union whose input branch
4519
4421
  * **is the wire**. A `z.coerce` cannot be published — zod renders the
4520
4422
  * coercion's result in either `io` direction, so the artifact would describe
4521
4423
  * a shape a query string can never carry.
@@ -4544,36 +4446,25 @@ var auditQuery = z15.object({
4544
4446
  /**
4545
4447
  * Only events by this actor.
4546
4448
  *
4547
- * **`z.uuid()`, because the column is one (Argus-W9, W9 review).** This was
4548
- * `z.string().min(1).max(200)`, so any non-uuid value reached Postgres as a
4549
- * uuid parameter and threw: `?actor_id=not-a-uuid` answered **500
4550
- * `internal_error`**, on the list route and the export alike.
4449
+ * **`z.uuid()`, because the column is one.** A looser string type lets any
4450
+ * non-uuid value reach the database as a uuid parameter, where the cast
4451
+ * throws: `?actor_id=not-a-uuid` then answers **500 `internal_error`** rather
4452
+ * than refusing the value.
4551
4453
  *
4552
- * Not a SQL-injection finding — Drizzle parameterises, and `' or 1=1--`
4553
- * failed at the same cast. It is a **500 where a 400 belongs**, and a 500 is
4554
- * the answer that explains nothing.
4555
- *
4556
- * The place is the part worth keeping: **this same wave pulled
4557
- * `refuseIfNotUuid` through ~15 call sites** so a typo could be told from a
4558
- * deletion — and the brand-new filter, whose field has exactly that shape,
4559
- * is the one that did not get it. A rule applied to the sites in front of
4560
- * you is not a rule applied to the class.
4454
+ * Not an injection question — the query is parameterised either way. It is a
4455
+ * **500 where a 400 belongs**, and a 500 is the answer that explains nothing.
4561
4456
  */
4562
4457
  actor_id: z15.uuid().optional(),
4563
4458
  /** Only events about this kind of target, e.g. `robot`. */
4564
4459
  target_kind: z15.string().min(1).max(40).optional(),
4565
4460
  /**
4566
4461
  * Absolute bounds in unix milliseconds, **half-open `[from, to)`** — the
4567
- * same rule the history shapes follow (DEF-062).
4462
+ * same rule the history shapes follow.
4568
4463
  *
4569
- * **Bounded to years 1..9999, and the bound is borrowed rather than
4570
- * invented.** `nonnegative()` alone let `253402300800000` (year 10000)
4571
- * through, where the Postgres bind path has no representation and the route
4572
- * answered 500 — measured either side of the edge: `253402300799000` → 200,
4573
- * `253402300800000` → 500 (Argus-W9). `history-query.ts`'s `parseTimeExprMs`
4574
- * already carries exactly this range, with M3's reasoning for why
4575
- * `Number.isSafeInteger` is wider than what a timestamp can be; this is that
4576
- * same number, not a second one that happens to agree.
4464
+ * **Bounded to years 1..9999.** `nonnegative()` alone admits instants a
4465
+ * timestamp column has no representation for, and the route answers 500
4466
+ * rather than refusing the value. `Number.isSafeInteger` is wider than what a
4467
+ * timestamp can be, so the bound is stated rather than inherited.
4577
4468
  */
4578
4469
  from_ms: auditTimestampMs.optional(),
4579
4470
  to_ms: auditTimestampMs.optional()
@@ -4596,7 +4487,7 @@ var auditListResponse = z15.object({
4596
4487
  next_cursor: z15.number().int().positive().nullable()
4597
4488
  });
4598
4489
 
4599
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/errors.js
4490
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/errors.js
4600
4491
  import { z as z16 } from "zod";
4601
4492
  var apiError = z16.object({
4602
4493
  code: z16.string().min(1),
@@ -4613,7 +4504,7 @@ var parameterInvalidDetails = z16.object({
4613
4504
  violations: z16.array(parameterViolation).min(1)
4614
4505
  });
4615
4506
 
4616
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/oauth.js
4507
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/oauth.js
4617
4508
  import { z as z17 } from "zod";
4618
4509
  var oauthErrorCode = z17.enum([
4619
4510
  "invalid_request",
@@ -4643,8 +4534,8 @@ var oauthError = z17.object({
4643
4534
  * makes the two indistinguishable to the caller, and *a field that cannot
4644
4535
  * express a distinction produces a workaround somewhere else*. Answering in
4645
4536
  * `apiError` instead would keep the distinction and hand an RFC-compliant
4646
- * client a body it cannot parse — which is the conformance this wave exists
4647
- * to provide.
4537
+ * client a body it cannot parse, which is the conformance this dialect
4538
+ * exists to provide.
4648
4539
  *
4649
4540
  * So both: `error` is what a standard client reads, `fleetless_code` is what
4650
4541
  * our own tooling switches on. RFC 6749 §5.2 permits additional members, and
@@ -4668,26 +4559,29 @@ var redirectUri = z17.string().min(1).max(2e3).refine((v) => {
4668
4559
  return false;
4669
4560
  }, { message: "redirect_uri must be an https URL, or http on an explicit loopback address, and carry no fragment" });
4670
4561
  var codeChallengeMethod = z17.enum(["S256"]);
4562
+ var MCP_DCR_MAX_REDIRECT_URIS = 5;
4671
4563
  var dynamicClientRegistrationRequest = z17.object({
4672
- client_name: z17.string().min(1).max(200).meta({
4673
- 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"*.'
4564
+ redirect_uris: z17.array(redirectUri).min(1).max(MCP_DCR_MAX_REDIRECT_URIS).meta({
4565
+ 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.`
4674
4566
  }),
4675
- redirect_uris: z17.array(redirectUri).min(1).max(20).meta({
4676
- 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."
4567
+ client_name: z17.string().min(1).max(200).optional().meta({
4568
+ 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"*.`
4569
+ }),
4570
+ token_endpoint_auth_method: z17.enum(["none"]).optional().meta({
4571
+ 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."
4677
4572
  }),
4678
4573
  grant_types: z17.array(z17.enum(["authorization_code", "refresh_token"])).optional().meta({
4679
- description: "Accepted and echoed back for conformance with RFC 7591. This server issues `authorization_code` and `refresh_token` and nothing else."
4574
+ 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."
4680
4575
  }),
4681
4576
  response_types: z17.array(z17.enum(["code"])).optional().meta({
4682
- description: "Accepted and echoed back for conformance. `code` is the only response type OAuth 2.1 leaves, the implicit grant having been removed."
4683
- }),
4684
- token_endpoint_auth_method: z17.enum(["none"]).optional().meta({
4685
- description: "`none`, RFC 7591's value for a public client. There is no client secret to hold: mandatory PKCE is the defence."
4577
+ 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."
4686
4578
  }),
4687
4579
  scope: z17.string().max(500).optional().meta({
4688
- description: "The scopes the client asks to be registered for, space-separated."
4580
+ 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."
4689
4581
  })
4690
- }).strict();
4582
+ }).meta({
4583
+ 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)."
4584
+ });
4691
4585
  var dynamicClientRegistrationResponse = z17.object({
4692
4586
  client_id: z17.string().min(1).max(200).meta({
4693
4587
  description: "The identifier this client sends at the authorize and token endpoints. Opaque, and not the app identifier."
@@ -4699,7 +4593,7 @@ var dynamicClientRegistrationResponse = z17.object({
4699
4593
  description: "The redirect URIs this registration was accepted for. A code is returned to one of these and nowhere else."
4700
4594
  }),
4701
4595
  grant_types: z17.array(z17.string()).meta({
4702
- description: "The grants this client may use: `authorization_code` and `refresh_token`."
4596
+ 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.'
4703
4597
  }),
4704
4598
  response_types: z17.array(z17.string()).meta({
4705
4599
  description: "The response types this client may ask for: `code`."
@@ -4714,45 +4608,28 @@ var dynamicClientRegistrationResponse = z17.object({
4714
4608
  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."
4715
4609
  })
4716
4610
  });
4717
- var oauthTokenRequest = z17.discriminatedUnion("grant_type", [
4718
- z17.object({
4719
- grant_type: z17.literal("authorization_code").meta({
4720
- description: "This request exchanges the code from the authorize redirect for tokens."
4721
- }),
4722
- code: z17.string().min(1).max(500).meta({
4723
- description: "The authorization code from the redirect. It may be exchanged once."
4724
- }),
4725
- redirect_uri: redirectUri.meta({
4726
- description: "The same redirect URI the authorize request used. It is compared, not merely recorded."
4727
- }),
4728
- client_id: z17.string().min(1).max(200).meta({
4729
- description: "The client making the exchange, as registered."
4730
- }),
4731
- code_verifier: z17.string().regex(/^[A-Za-z0-9\-._~]{43,128}$/, "code_verifier must be 43-128 unreserved characters (RFC 7636 \xA74.1)").meta({
4732
- 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."
4733
- }),
4734
- resource: z17.url().optional().meta({
4735
- 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."
4736
- })
4611
+ var oauthTokenRequest = z17.object({
4612
+ grant_type: z17.literal("authorization_code").meta({
4613
+ 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."
4737
4614
  }),
4738
- z17.object({
4739
- grant_type: z17.literal("refresh_token").meta({
4740
- description: "This request trades a refresh token for a fresh access token."
4741
- }),
4742
- refresh_token: z17.string().min(1).max(500).meta({
4743
- description: "The refresh token to spend. Refresh tokens rotate, and presenting one twice is treated as theft rather than as a retry."
4744
- }),
4745
- client_id: z17.string().min(1).max(200).meta({
4746
- description: "The client refreshing, as registered."
4747
- }),
4748
- resource: z17.url().optional().meta({
4749
- 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."
4750
- }),
4751
- scope: z17.string().max(500).optional().meta({
4752
- description: "A narrower scope for the successor token. RFC 6749 \xA76 lets a refresh narrow scope, never widen it."
4753
- })
4615
+ code: z17.string().min(1).max(500).meta({
4616
+ 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."
4617
+ }),
4618
+ redirect_uri: redirectUri.meta({
4619
+ description: "The same redirect URI the authorize request used. It is compared, not merely recorded."
4620
+ }),
4621
+ client_id: z17.string().min(1).max(200).meta({
4622
+ description: "The client making the exchange, as registered."
4623
+ }),
4624
+ code_verifier: z17.string().regex(/^[A-Za-z0-9\-._~]{43,128}$/, "code_verifier must be 43-128 unreserved characters (RFC 7636 \xA74.1)").meta({
4625
+ 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."
4626
+ }),
4627
+ resource: z17.url().optional().meta({
4628
+ 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."
4754
4629
  })
4755
- ]);
4630
+ }).meta({
4631
+ 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."
4632
+ });
4756
4633
  var oauthTokenResponse = z17.object({
4757
4634
  access_token: z17.string().min(1).meta({
4758
4635
  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."
@@ -4837,20 +4714,18 @@ var oauthAuthorizeQuery = z17.object({
4837
4714
  }),
4838
4715
  resource: z17.string().optional().meta({
4839
4716
  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."
4840
- }),
4841
- scope: z17.string().optional().meta({
4842
- 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."
4843
4717
  })
4718
+ // **No `scope`, because this authorization server issues none.** The field
4719
+ // was here describing itself as "carried onto the interaction and read
4720
+ // again at consent"; neither authorize handler reads it, the interaction
4721
+ // row has no column for it, and the consent screen answers `scopes: []`.
4722
+ // A parameter documented as carried and in fact dropped is worse than one
4723
+ // that is absent.
4844
4724
  }).meta({
4845
- 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."
4725
+ 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."
4846
4726
  });
4847
- var oauthRegisterQuery = z17.object({
4848
- app_identifier: z17.string().min(1).meta({
4849
- 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`)."
4850
- })
4851
- }).meta({ description: "The app a dynamic client registers under \u2014 the one parameter RFC 7591 has no body field for." });
4852
4727
 
4853
- // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_87d1585beff942190ebb8b5dc4e89225/node_modules/@fleetless/contracts/dist/routes.js
4728
+ // node_modules/.pnpm/@fleetless+contracts@1.0.5/node_modules/@fleetless/contracts/dist/routes.js
4854
4729
  var MCP_APP = MCP_APP_PATHS(":appIdentifier");
4855
4730
  var APP_IDENTIFIER = {
4856
4731
  name: "appIdentifier",
@@ -5413,7 +5288,7 @@ var ROUTES = [
5413
5288
  response: appUser,
5414
5289
  errors: [...DEVELOPER_GUARD, "invalid_uuid", "validation_error", "not_found", "email_taken", "target_state_conflict", "quota_exceeded"],
5415
5290
  transport: "http",
5416
- 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."
5291
+ 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."
5417
5292
  },
5418
5293
  {
5419
5294
  method: "GET",
@@ -5526,7 +5401,7 @@ var ROUTES = [
5526
5401
  response: null,
5527
5402
  errors: [...DEVELOPER_GUARD, "invalid_uuid", "not_found"],
5528
5403
  transport: "http",
5529
- 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."
5404
+ 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`)."
5530
5405
  },
5531
5406
  {
5532
5407
  method: "GET",
@@ -5600,7 +5475,7 @@ var ROUTES = [
5600
5475
  transport: "http",
5601
5476
  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."
5602
5477
  },
5603
- /* --------------------------------------- the app's OIDC providers (D4) */
5478
+ /* ------------------------------------------- the app's OIDC providers */
5604
5479
  {
5605
5480
  method: "GET",
5606
5481
  path: "/api/apps/:id/oidc-providers",
@@ -6101,11 +5976,11 @@ var ROUTES = [
6101
5976
  status: 201,
6102
5977
  params: [],
6103
5978
  query: null,
6104
- request: null,
5979
+ request: dynamicClientRegistrationRequest,
6105
5980
  response: dynamicClientRegistrationResponse,
6106
5981
  errors: ["rate_limited"],
6107
5982
  transport: "http",
6108
- 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`."
5983
+ 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`."
6109
5984
  },
6110
5985
  {
6111
5986
  method: "GET",
@@ -6118,12 +5993,12 @@ var ROUTES = [
6118
5993
  ownerTier: false,
6119
5994
  status: 302,
6120
5995
  params: [],
6121
- query: null,
5996
+ query: oauthAuthorizeQuery,
6122
5997
  request: null,
6123
5998
  response: null,
6124
5999
  errors: [],
6125
6000
  transport: "http",
6126
- 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."
6001
+ 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."
6127
6002
  },
6128
6003
  {
6129
6004
  method: "GET",
@@ -6159,7 +6034,7 @@ var ROUTES = [
6159
6034
  response: null,
6160
6035
  errors: ["rate_limited", "validation_error", "token_spent"],
6161
6036
  transport: "http",
6162
- 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.'
6037
+ 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.'
6163
6038
  },
6164
6039
  {
6165
6040
  method: "POST",
@@ -6227,7 +6102,7 @@ var ROUTES = [
6227
6102
  status: 200,
6228
6103
  params: [],
6229
6104
  query: null,
6230
- request: null,
6105
+ request: oauthTokenRequest,
6231
6106
  response: oauthTokenResponse,
6232
6107
  errors: [],
6233
6108
  transport: "http",
@@ -6413,9 +6288,9 @@ var ROUTES = [
6413
6288
  response: null,
6414
6289
  errors: ["unauthorized", "forbidden"],
6415
6290
  transport: "http",
6416
- 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."
6291
+ 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."
6417
6292
  },
6418
- /* ----------------------------------------- mcp (one app's own server, D7) */
6293
+ /* --------------------------------------------- mcp (one app's own server) */
6419
6294
  {
6420
6295
  method: "POST",
6421
6296
  path: MCP_APP.endpoint,
@@ -6432,7 +6307,7 @@ var ROUTES = [
6432
6307
  response: null,
6433
6308
  errors: ["not_found", "unauthorized", "forbidden"],
6434
6309
  transport: "http",
6435
- 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`."
6310
+ 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`."
6436
6311
  },
6437
6312
  {
6438
6313
  method: "GET",
@@ -6450,7 +6325,7 @@ var ROUTES = [
6450
6325
  response: null,
6451
6326
  errors: ["not_found", "unauthorized", "forbidden"],
6452
6327
  transport: "http",
6453
- 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."
6328
+ 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."
6454
6329
  },
6455
6330
  {
6456
6331
  method: "DELETE",
@@ -6504,7 +6379,7 @@ var ROUTES = [
6504
6379
  response: authorizationServerMetadata,
6505
6380
  errors: ["not_found"],
6506
6381
  transport: "http",
6507
- 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."
6382
+ 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."
6508
6383
  },
6509
6384
  {
6510
6385
  method: "POST",
@@ -6518,11 +6393,11 @@ var ROUTES = [
6518
6393
  status: 201,
6519
6394
  params: [APP_IDENTIFIER],
6520
6395
  query: null,
6521
- request: null,
6396
+ request: dynamicClientRegistrationRequest,
6522
6397
  response: dynamicClientRegistrationResponse,
6523
6398
  errors: ["rate_limited", "not_found"],
6524
6399
  transport: "http",
6525
- 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."
6400
+ 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."
6526
6401
  },
6527
6402
  {
6528
6403
  method: "GET",
@@ -6535,12 +6410,12 @@ var ROUTES = [
6535
6410
  ownerTier: false,
6536
6411
  status: 302,
6537
6412
  params: [APP_IDENTIFIER],
6538
- query: null,
6413
+ query: oauthAuthorizeQuery,
6539
6414
  request: null,
6540
6415
  response: null,
6541
6416
  errors: ["not_found", "target_state_conflict"],
6542
6417
  transport: "http",
6543
- 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.'
6418
+ 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.'
6544
6419
  },
6545
6420
  {
6546
6421
  method: "POST",
@@ -6554,7 +6429,7 @@ var ROUTES = [
6554
6429
  status: 200,
6555
6430
  params: [APP_IDENTIFIER],
6556
6431
  query: null,
6557
- request: null,
6432
+ request: oauthTokenRequest,
6558
6433
  response: oauthTokenResponse,
6559
6434
  errors: [],
6560
6435
  transport: "http",
@@ -6812,7 +6687,7 @@ var ROUTES = [
6812
6687
  response: null,
6813
6688
  errors: ["rate_limited"],
6814
6689
  transport: "http",
6815
- 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."
6690
+ 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."
6816
6691
  },
6817
6692
  {
6818
6693
  method: "POST",
@@ -6832,7 +6707,7 @@ var ROUTES = [
6832
6707
  transport: "http",
6833
6708
  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."
6834
6709
  },
6835
- /* --------------------------- the app's own MCP consent screen (D7) */
6710
+ /* ------------------------------- the app's own MCP consent screen */
6836
6711
  {
6837
6712
  method: "GET",
6838
6713
  path: "/api/client/mcp/interactions/:id",
@@ -6867,7 +6742,7 @@ var ROUTES = [
6867
6742
  response: clientMcpInteractionDecisionResponse,
6868
6743
  errors: [...CLIENT_GUARD, "rate_limited", "interaction_expired", "mcp_disabled"],
6869
6744
  transport: "http",
6870
- 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."
6745
+ 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."
6871
6746
  },
6872
6747
  {
6873
6748
  method: "POST",
@@ -6904,7 +6779,7 @@ var ROUTES = [
6904
6779
  response: mcpConsentGrantListResponse,
6905
6780
  errors: [...CLIENT_GUARD],
6906
6781
  transport: "http",
6907
- 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."
6782
+ 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."
6908
6783
  },
6909
6784
  {
6910
6785
  method: "DELETE",
@@ -6922,7 +6797,7 @@ var ROUTES = [
6922
6797
  response: null,
6923
6798
  errors: [...CLIENT_GUARD],
6924
6799
  transport: "http",
6925
- 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."
6800
+ 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."
6926
6801
  },
6927
6802
  /* ------------------------------------------------------------- robots */
6928
6803
  {
@@ -7415,7 +7290,7 @@ var ROUTES = [
7415
7290
  response: jobRunListResponse,
7416
7291
  errors: [...CLIENT_GUARD, "invalid_uuid", "not_found", "capability_required", "validation_error"],
7417
7292
  transport: "http",
7418
- 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."
7293
+ 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."
7419
7294
  },
7420
7295
  {
7421
7296
  method: "POST",
@@ -7917,7 +7792,7 @@ var ROUTES = [
7917
7792
  response: asset,
7918
7793
  errors: ["unauthorized", "rate_limited", "asset_too_large", "validation_error", "not_found", "quota_exceeded", "bad_request"],
7919
7794
  transport: "http",
7920
- 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."
7795
+ 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."
7921
7796
  },
7922
7797
  /* ------------------------------------ realtime and bridge transports */
7923
7798
  {
@@ -8434,7 +8309,7 @@ function createRealtimeCommandTransport(channel) {
8434
8309
  return {
8435
8310
  // Declared `async` deliberately, unlike `cancel`/`publish` below: it is
8436
8311
  // the only one of the three that can refuse *before* sending anything
8437
- // (resolveLocalWaitMs's `invalid_option`, D3a), and every caller of
8312
+ // (`resolveLocalWaitMs`'s `invalid_option`), and every caller of
8438
8313
  // this interface — starting with this file's own `sendCommand` callers
8439
8314
  // — is entitled to assume `CommandTransport.invoke` always returns a
8440
8315
  // promise rather than throwing synchronously. Without `async` here, a
@@ -8454,7 +8329,7 @@ function createRealtimeCommandTransport(channel) {
8454
8329
  };
8455
8330
  return sendCommand(channel, frame, { timeoutMs });
8456
8331
  },
8457
- // Declared `async` for the same reason `invoke` above is (D3a, D6):
8332
+ // Declared `async` for the same reason `invoke` above is:
8458
8333
  // `assertValidJobId` can throw before a frame is ever built, and a
8459
8334
  // caller of this interface is entitled to a rejected promise, never a
8460
8335
  // thrown exception.