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