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