@fleetless/sdk 2.0.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -543,7 +543,7 @@ type ParameterInvalidDetails = z.infer<typeof parameterInvalidDetails>;
543
543
  * list is the shared vocabulary, not a closed set, so a new refusal never
544
544
  * needs a contracts release before it can be reported honestly.
545
545
  */
546
- declare const ERROR_CODES: readonly ["not_found", "validation_error", "bad_request", "unknown_datapoint", "invalid_token", "protocol_mismatch", "invalid_frame", "duplicate_slug", "reserved_slug", "unknown_slug", "unknown_field_path", "unknown_type", "unknown_topic", "invalid_rate", "invalid_range", "config_conflict", "no_data", "robot_offline", "bridge_timeout", "unauthorized", "forbidden", "invalid_credentials", "token_expired", "token_revoked", "invite_expired", "invite_used", "email_taken", "identifier_taken", "weak_password", "account_blocked", "busy", "parameter_invalid", "job_lost", "publisher_busy", "unknown_command", "not_subscribable", "camera_offline", "no_snapshot_yet", "live_unavailable", "wrong_kind", "not_recorded", "not_aggregatable", "quota_exceeded", "credential_in_use", "goal_timeout", "robot_in_use", "robot_deletion_partial", "job_queue_full", "invalid_uuid", "rate_limited", "tier_required", "token_spent", "service_timeout", "asset_missing", "asset_too_large", "dynamic_registration_disabled", "client_limit_reached", "identity_conflict", "identity_not_provisioned", "idp_unavailable", "mcp_disabled", "tool_not_available", "capability_required", "last_owner", "group_not_deletable", "group_in_use", "target_state_conflict", "mcp_access_denied", "signup_closed"];
546
+ declare const ERROR_CODES: readonly ["not_found", "validation_error", "bad_request", "unknown_datapoint", "invalid_token", "protocol_mismatch", "invalid_frame", "duplicate_slug", "reserved_slug", "unknown_slug", "unknown_field_path", "unknown_type", "unknown_topic", "invalid_rate", "invalid_range", "config_conflict", "no_data", "robot_offline", "bridge_timeout", "unauthorized", "forbidden", "invalid_credentials", "token_expired", "token_revoked", "invite_expired", "invite_used", "email_taken", "identifier_taken", "weak_password", "account_blocked", "busy", "parameter_invalid", "job_lost", "publisher_busy", "unknown_command", "not_subscribable", "camera_offline", "no_snapshot_yet", "live_unavailable", "wrong_kind", "not_recorded", "not_aggregatable", "quota_exceeded", "credential_in_use", "goal_timeout", "robot_in_use", "robot_deletion_partial", "job_queue_full", "invalid_uuid", "rate_limited", "tier_required", "token_spent", "service_timeout", "asset_missing", "asset_too_large", "dynamic_registration_disabled", "client_limit_reached", "identity_conflict", "identity_not_provisioned", "idp_unavailable", "mcp_disabled", "tool_not_available", "capability_required", "last_owner", "group_not_deletable", "group_in_use", "target_state_conflict", "mcp_access_denied", "signup_closed", "draft_not_a_document", "internal_error", "not_cancellable", "unsupported_media_type", "wrong_browser", "invalid_yaml", "unstorable_yaml"];
547
547
  type ErrorCode = (typeof ERROR_CODES)[number];
548
548
 
549
549
  /**
@@ -620,7 +620,7 @@ type ConsentRevokeResponse = z.infer<typeof consentRevokeResponse>;
620
620
  * Codes the SDK produces itself rather than relaying from the server. Kept
621
621
  * out of `@fleetless/contracts`' `ERROR_CODES` deliberately — that list is
622
622
  * the *wire* vocabulary, every entry something a server may actually send,
623
- * and none of these are. Two kinds:
623
+ * and none of these are.
624
624
  *
625
625
  * - `no_session` / `no_websocket`: a client-side refusal *before* a request
626
626
  * ever reaches the network (not logged in; no WebSocket implementation
@@ -628,102 +628,106 @@ type ConsentRevokeResponse = z.infer<typeof consentRevokeResponse>;
628
628
  * caller can tell "the server refused me" from "the SDK refused before
629
629
  * asking" by the code alone.
630
630
  * - `unparseable_error`: the opposite direction — a real response *did*
631
- * arrive, its body just wasn't shaped like §11.5's error format. Not a
632
- * refusal at all, just "we don't know what the server said."
633
- * - `command_timeout` (W4): a realtime command (`invoke`/`cancel`/`publish`)
634
- * got no `command_result` within its timeout. The server may still answer
631
+ * arrive, its body just was not shaped like the platform's error format.
632
+ * Not a refusal at all, just "we do not know what the server said."
633
+ * - `command_timeout`: a realtime command (`invoke`/`cancel`/`publish`) got
634
+ * no `command_result` within its timeout. The server may still answer
635
635
  * later on the same socket — nobody knows — but the caller cannot be made
636
636
  * to wait forever for that.
637
- * - `command_outcome_unknown` (W4): worse than a timeout, and told apart
638
- * from it on purpose — the realtime connection that carried the command
639
- * was replaced by a new one (a reconnect) before any reply arrived. A
640
- * reply can now never come: the server, if it answered at all, answered a
637
+ * - `command_outcome_unknown`: worse than a timeout, and told apart from it
638
+ * on purpose — the realtime connection that carried the command was
639
+ * replaced by a new one (a reconnect) before any reply arrived. A reply
640
+ * can now never come: the server, if it answered at all, answered a
641
641
  * socket that no longer exists. The command may or may not have run.
642
642
  * Never retried automatically — that could run an action twice — the
643
643
  * caller recovers by reading the job (e.g. `actions.subscribe`), since
644
644
  * state is observed by slug regardless of which connection asked for it.
645
- * - `unexpected_response` (W4): the server answered `ok:true` but left out
645
+ * - `unexpected_response`: the server answered `ok:true` but left out
646
646
  * something the command is defined to always return (e.g. no `job` on a
647
647
  * successful `invoke`) — a contract violation the SDK noticed, not a
648
648
  * refusal.
649
- * - `invalid_option` (W6b): the caller passed an *SDK-level* argument or
650
- * option that cannot mean what it looks like it means. Two cases so far:
651
- * `timeoutMs < patienceMs` on `invoke`/`call` (see `resolveLocalWaitMs` in
652
- * `commands.ts`) — the SDK would give up locally before the platform's own
653
- * patience runs out, and report `command_timeout` for a call the platform
654
- * never actually refused; and a non-string, non-null, non-omitted `jobId`
655
- * on `cancel` (D6, see `assertValidJobId`) — almost always a caller who
656
- * upgraded past the pre-W6b `cancel(robotId, slug, options?)` signature
657
- * and is still passing an options object third. Both are thrown
658
- * synchronously, before any request is sent a client-side mistake to
659
- * fix, not something a server response could ever produce, which is why
660
- * this code belongs here and not in `@fleetless/contracts`' `ERROR_CODES`.
661
- * - `untrusted_absolute_url` (W7): `HttpClient` refused to fetch an absolute
662
- * URL whose origin does not match this client's own configured `baseUrl`
663
- * thrown before the request is ever sent, so no `Authorization` header
664
- * is ever built for it, let alone attached. The one caller that hands
665
- * `HttpClient` an absolute URL at all is `assets.createMeshLoader`
666
- * (§4.6), fetching a URDF's rewritten mesh URIs and a URDF is ROS graph
667
- * input, not first-party data, so an app rendering one must not silently
668
- * trust wherever it points. `assets.createMeshLoader`'s `onComplete`
669
- * surfaces this the same way it surfaces a network failure: `(null, err)`.
670
- * - `no_urdf_synced` (W7a): `assets.prepareUrdfScene` looked for a
671
- * `kind: 'urdf'` row in `assets.list()` and found none. Thrown before any
672
- * asset fetch, rather than left to surface as a confusing downstream
673
- * failure from `URDFLoader.parse(undefined)` or similar the caller's
674
- * fix is "sync a URDF first" (console, Owner-tier, §4.6), which this
675
- * error can say directly.
676
- * - `no_hosted_login_attempt` (W7b, Momus's review): `auth.completeHostedLogin()`
677
- * was called with an empty `expectedState` nothing was persisted for
678
- * this attempt. A callback landing in a different tab or window than the
679
- * one that called `beginHostedLogin`, a restored session, or storage
680
- * cleared in between all produce exactly this, and none of them is an
681
- * attack. Told apart from `state_mismatch` on purpose: the two diagnoses
682
- * have different remedies ("check how you persisted the value" versus
683
- * "this response belongs to a login you did not start"), the same
684
- * reasoning this wave already applied once to `identity_conflict` versus
685
- * `identity_not_provisioned`. Also closes a real gap — comparing two
686
- * *empty* strings with `!==` is `false`, so without this check first, a
687
- * caller with nothing persisted at all could reach `state_mismatch`'s
688
- * comparison having contributed no defence whatsoever.
689
- * - `aborted` (register row 244, W7c): `assets.prepareUrdfScene()` was given
690
- * an `AbortSignal` and it fired — either already-aborted before the call
691
- * started, or mid-flight while a fetch was in progress. Normalized to this
692
- * one code regardless of which stage the abort landed in, rather than
693
- * surfacing whatever shape the underlying `fetch()` rejects an aborted
694
- * request with (a `DOMException` named `AbortError` in a browser, an
695
- * `Error` named `AbortError` under Node's `fetch` — two different shapes a
696
- * caller would otherwise have to detect themselves to tell "I cancelled
697
- * this" from "the network actually failed"). Every partial resource this
698
- * call had already created (`blob:` URLs) is revoked before this throws —
699
- * an aborted load must not leak what it fetched before the signal fired,
700
- * the same guarantee a failed load already had (D6).
701
- * - `state_mismatch` (W7b): `auth.completeHostedLogin()` was called with a
702
- * `state` that does not match the `expectedState` its own `beginHostedLogin()`
649
+ * - `invalid_option`: the caller passed an *SDK-level* argument or option
650
+ * that cannot mean what it looks like it means. Three cases so far:
651
+ * `timeoutMs < patienceMs` on `invoke`/`call` the SDK would give up
652
+ * locally before the platform's own patience runs out, and report
653
+ * `command_timeout` for a call the platform never actually refused; a
654
+ * non-string, non-null, non-omitted `jobId` on `cancel` — almost always a
655
+ * caller who upgraded past the older `cancel(robotId, slug, options?)`
656
+ * signature and is still passing an options object third; and a
657
+ * `concurrency` on `assets.prepareUrdfScene` that is not a positive
658
+ * integer, which would otherwise fetch nothing and return a scene that
659
+ * renders blank with no error to explain why. All three are refused
660
+ * before any request is sent as a rejection, since every one of those
661
+ * methods is `async`. A client-side mistake to fix, not something a
662
+ * server response could ever produce, which is why this code belongs here
663
+ * and not in `@fleetless/contracts`' `ERROR_CODES`.
664
+ * - `untrusted_absolute_url`: the SDK refused to fetch an absolute URL
665
+ * whose origin does not match this client's own `apiUrl` — thrown before
666
+ * the request is ever sent, so no `Authorization` header is ever built for
667
+ * it, let alone attached. The one caller that fetches an absolute URL at
668
+ * all is `assets.createMeshLoader`, following a URDF's rewritten mesh
669
+ * URIs and a URDF is ROS graph input, not first-party data, so an app
670
+ * rendering one must not silently trust wherever it points.
671
+ * `assets.createMeshLoader`'s `onComplete` surfaces this the same way it
672
+ * surfaces a network failure: `(null, err)`.
673
+ * - `no_urdf_synced`: `assets.prepareUrdfScene` looked for a `kind: 'urdf'`
674
+ * row in `assets.list()` and found none. Thrown before any asset fetch,
675
+ * rather than left to surface as a confusing downstream failure from
676
+ * `URDFLoader.parse(undefined)` or similar — the caller's fix is "sync a
677
+ * URDF first", which this error can say directly.
678
+ * - `no_hosted_login_attempt`: `auth.completeHostedLogin()` was called with
679
+ * an empty `expectedState` nothing was persisted for this attempt. A
680
+ * callback landing in a different tab or window than the one that called
681
+ * `beginHostedLogin`, a restored session, or storage cleared in between
682
+ * all produce exactly this, and none of them is an attack. Told apart
683
+ * from `state_mismatch` on purpose: the two diagnoses have different
684
+ * remedies ("check how you persisted the value" versus "this response
685
+ * belongs to a login you did not start"). It also closes a real gap —
686
+ * comparing two *empty* strings with `!==` is `false`, so without this
687
+ * check first, a caller with nothing persisted at all could reach
688
+ * `state_mismatch`'s comparison having contributed no defence whatsoever.
689
+ * - `aborted`: `assets.prepareUrdfScene()` was given an `AbortSignal` and it
690
+ * fired — either already-aborted before the call started, or mid-flight
691
+ * while a fetch was in progress. Normalized to this one code regardless of
692
+ * which stage the abort landed in, rather than surfacing whatever shape
693
+ * the underlying `fetch()` rejects an aborted request with (a
694
+ * `DOMException` named `AbortError` in a browser, an `Error` named
695
+ * `AbortError` under Node's `fetch` — two different shapes a caller would
696
+ * otherwise have to detect themselves to tell "I cancelled this" from "the
697
+ * network actually failed"). Every partial resource this call had already
698
+ * created (`blob:` URLs) is revoked before this throws — an aborted load
699
+ * must not leak what it fetched before the signal fired, the same
700
+ * guarantee a failed load already had.
701
+ * - `state_mismatch`: `auth.completeHostedLogin()` was called with a `state`
702
+ * that does not match the `expectedState` its own `beginHostedLogin()`
703
703
  * returned for this attempt (or with no `state` at all — `beginHostedLogin`
704
704
  * always sets one, so a callback carrying none does not look like a reply
705
705
  * to a flow this client started). Thrown before `/oauth/token` is ever
706
- * called RFC 6749 §10.12's whole point is that a caller must not
706
+ * called: RFC 6749 section 10.12's whole point is that a caller must not
707
707
  * complete an authorization response it did not itself request, so this
708
708
  * check happens client-side, first, rather than being left to the server
709
- * to catch (by which point a code exchange would already have been
710
- * attempted for a flow this client never started).
709
+ * to catch by which point a code exchange would already have been
710
+ * attempted for a flow this client never started.
711
711
  */
712
712
  declare const SDK_ERROR_CODES: readonly ["no_session", "no_websocket", "unparseable_error", "command_timeout", "command_outcome_unknown", "unexpected_response", "invalid_option", "untrusted_absolute_url", "state_mismatch", "no_hosted_login_attempt", "no_urdf_synced", "aborted"];
713
+ /**
714
+ * The union of `SDK_ERROR_CODES` — the SDK's own client-side error
715
+ * vocabulary. A `FleetlessError` whose `code` is one of these was raised by
716
+ * this SDK rather than relayed from the server.
717
+ */
713
718
  type SdkErrorCode = (typeof SDK_ERROR_CODES)[number];
714
719
  /**
715
720
  * A stable code a caller can branch on: a server-defined code (open-ended —
716
- * see `ErrorCode`'s own doc comment), one of the SDK's own client-side
717
- * codes above, or, since neither list is exhaustive, any other string.
721
+ * see `ErrorCode`'s own doc comment), one of `SdkErrorCode`'s client-side
722
+ * codes, or, since neither list is exhaustive, any other string.
718
723
  * `(string & {})` is the standard trick to keep autocomplete on the known
719
724
  * values while still accepting an arbitrary one.
720
725
  */
721
726
  type FleetlessErrorCode = ErrorCode | SdkErrorCode | (string & {});
722
727
  /**
723
- * The one error type the SDK throws for a refused API call (spec §11.5): a
724
- * stable machine-readable `code` a caller can branch on (`forbidden` vs
725
- * `token_expired`) plus a human `message` for logs and debugging. Never
726
- * parse `message` — it is not part of the contract, only `code` is.
728
+ * The third argument of `FleetlessError`'s constructor: what a thrower can
729
+ * attach beyond the code and the message. Both fields are optional, and
730
+ * both are absent on the codes the SDK raises before any request is sent.
727
731
  */
728
732
  interface FleetlessErrorOptions {
729
733
  /** Field-level detail for validation errors, passed through verbatim. */
@@ -731,79 +735,126 @@ interface FleetlessErrorOptions {
731
735
  /** The HTTP status of the response that produced this error, if any. */
732
736
  status?: number;
733
737
  }
738
+ /**
739
+ * The one error type the SDK throws for a refused API call: a stable
740
+ * machine-readable `code` a caller can branch on (`forbidden` versus
741
+ * `token_expired`) plus a human `message` for logs and debugging. Never
742
+ * parse `message` — it is not part of the contract, only `code` is.
743
+ *
744
+ * Catch it by shape rather than by class where you can (`err.code`), since
745
+ * a bundler that ends up with two copies of the SDK also ends up with two
746
+ * classes and `instanceof` then answers `false` for a genuine one.
747
+ */
734
748
  declare class FleetlessError extends Error {
749
+ /** What went wrong, as a stable string — the field to branch on. */
735
750
  readonly code: FleetlessErrorCode;
751
+ /**
752
+ * Structured detail the server sent with the refusal, if any: the
753
+ * violations behind `parameter_invalid`, the running job behind `busy`,
754
+ * `retry_after_ms` behind `rate_limited`. Parse it rather than assume
755
+ * its shape — `parameterInvalidDetails` is exported for exactly that.
756
+ */
736
757
  readonly details?: unknown;
758
+ /** The HTTP status of the response that produced this error, if it came from one. */
737
759
  readonly status?: number;
760
+ /**
761
+ * Builds an error. `code` is what a caller branches on and `message` is
762
+ * for a human; anything else the refusal carried goes in `options`.
763
+ */
738
764
  constructor(code: FleetlessErrorCode, message: string, options?: FleetlessErrorOptions);
739
765
  }
740
766
 
767
+ /**
768
+ * The options every realtime command accepts — `actions.cancel` and
769
+ * `publishers.publish` take exactly these; `InvokeOptions` extends them for
770
+ * `actions.invoke` and `services.call`.
771
+ */
741
772
  interface SendCommandOptions {
742
773
  /** How long to wait for a `command_result` before rejecting `command_timeout`. Default 10s. */
743
774
  timeoutMs?: number;
744
775
  }
745
776
  /**
746
- * `invoke`-only: how long **the platform itself** should wait for this one
747
- * call before giving up on the robot (W6b) the whole wait for a service
748
- * call, goal *acceptance* only for an action (once accepted, a job runs as
749
- * long as it runs and is observed, not awaited).
750
- *
751
- * Optional; absent means the platform's own default (`DEFAULT_PATIENCE_MS`,
752
- * 15s) — exactly today's behaviour for a caller who names no preference.
753
- * Outside `[MIN_PATIENCE_MS, MAX_PATIENCE_MS]` the platform refuses with
754
- * `validation_error` rather than clamping, and this SDK does not clamp
755
- * locally or retry — surfacing the refusal is the whole of what it does
756
- * with this field. The floor exists because impatience reaches the robot,
757
- * not just the platform: a patience too short to survive a goal-acceptance
758
- * round trip made the bridge report `goal_timeout` and then issue a
759
- * *corrective cancel* against a goal an action server accepted a moment
760
- * later — a caller who names an unreachable deadline was causing a real
761
- * cancellation on the machine, repeatably, not just receiving an error.
762
- *
763
- * `timeoutMs` bounds how long *this SDK* waits locally for a reply on the
764
- * wire it already sent on; `patienceMs` travels to the platform and bounds
765
- * what *it* is willing to wait for from the robot. **They are not set
766
- * independently of each other, and D3a exists because the first version of
767
- * this doc comment said they were.** `timeoutMs` left unset is *derived*
768
- * from `patienceMs` (see `resolveLocalWaitMs`), not defaulted to a fixed
769
- * number that might be shorter — the SDK giving up locally before the
770
- * platform's own deadline would report `command_timeout` for a call the
771
- * platform never actually refused, which is exactly the two-clocks defect
772
- * this wave exists to remove, just relocated into this SDK instead of
773
- * between the cloud and the bridge. Setting both explicitly with
774
- * `timeoutMs < patienceMs` is refused with `invalid_option` before any
775
- * request is sent, for the same reason.
777
+ * What `actions.invoke` and `services.call` accept on top of
778
+ * `SendCommandOptions`: the two clocks a command runs under, one local to
779
+ * this SDK and one on the platform.
776
780
  */
777
781
  interface InvokeOptions extends SendCommandOptions {
782
+ /**
783
+ * How long **the platform itself** should wait for this one call before
784
+ * giving up on the robot — the whole wait for a service call, goal
785
+ * *acceptance* only for an action (once accepted, a job runs as long as it
786
+ * runs and is observed, not awaited).
787
+ *
788
+ * Optional; absent means the platform's own `DEFAULT_PATIENCE_MS`, 15s —
789
+ * exactly the behaviour of a caller who names no preference. Outside
790
+ * `MIN_PATIENCE_MS` to `MAX_PATIENCE_MS` (1s to 120s, both exported by
791
+ * `@fleetless/contracts`) the platform refuses with `validation_error`
792
+ * rather than clamping, and this SDK does not clamp locally or retry:
793
+ * surfacing the refusal is the whole of what it does with this field.
794
+ * The floor exists because
795
+ * impatience reaches the robot, not just the platform — a patience too
796
+ * short to survive a goal-acceptance round trip made the bridge report
797
+ * `goal_timeout` and then issue a *corrective cancel* against a goal an
798
+ * action server accepted a moment later, so a caller who names an
799
+ * unreachable deadline was causing a real cancellation on the machine,
800
+ * repeatably, not just receiving an error.
801
+ *
802
+ * `timeoutMs` bounds how long *this SDK* waits locally for a reply on the
803
+ * wire it already sent on; `patienceMs` travels to the platform and bounds
804
+ * what *it* is willing to wait for from the robot. **They are not set
805
+ * independently of each other.** `timeoutMs` left unset is *derived* from
806
+ * `patienceMs`, not defaulted to a fixed number that might be shorter —
807
+ * the SDK giving up locally before the platform's own deadline would
808
+ * report `command_timeout` for a call the platform never actually refused.
809
+ * Setting both explicitly with `timeoutMs < patienceMs` is refused with
810
+ * `invalid_option` before any request is sent, for the same reason.
811
+ */
778
812
  patienceMs?: number;
779
813
  }
780
814
 
815
+ /**
816
+ * The callbacks `actions.subscribe` reports through: one for every job
817
+ * update on the slug, one for a refusal of the subscription itself.
818
+ */
781
819
  interface JobSubscriptionHandlers {
782
820
  /** Called on every update pushed for the slug's current job — state, feedback, progress and result. */
783
821
  onJob(event: JobEvent): void;
784
- /** Called once if the subscription is refused (§11.5: e.g. `forbidden`, an unknown slug). */
822
+ /** Called once if the subscription is refused, e.g. `forbidden` or an unknown slug. */
785
823
  onError?(error: FleetlessError): void;
786
824
  }
825
+ /** A live job subscription, returned by `actions.subscribe`. */
787
826
  interface JobSubscription {
788
- /** Stops the subscription and, if the channel is currently connected, tells the server. */
827
+ /**
828
+ * Stops this subscription. The `unsubscribe` frame reaches the server only
829
+ * when this was the **last** holder of the robot and slug pair, and only if
830
+ * the channel is connected — subscriptions are reference-counted across
831
+ * kinds, so releasing this one while a second `actions.subscribe`, a
832
+ * `datapoints.subscribe` or an in-flight `services.call` still holds the
833
+ * same pair leaves that one's stream running untouched. Safe to call more
834
+ * than once.
835
+ */
789
836
  unsubscribe(): void;
790
837
  }
791
838
 
839
+ /**
840
+ * Long-running work on a robot, reachable as `client.actions`. An action is
841
+ * a ROS action the developer exposed under a slug: it is invoked, it runs
842
+ * for as long as it runs, and it reports back while it does.
843
+ */
792
844
  interface ActionsApi {
793
845
  /**
794
- * Invokes an action (spec §11.3). Resolves as soon as the job is created
846
+ * Invokes an action. Resolves as soon as the job is created
795
847
  * — the job id is informative, not the result. Feedback, progress and the
796
848
  * eventual result arrive separately over `subscribe`. A second invoke of
797
849
  * the same slug while one is already running is refused `busy`, with
798
850
  * `error.details.running` naming the job that is running.
799
851
  *
800
- * `options.patienceMs` bounds goal *acceptance* only (W6b) — once a goal
852
+ * `options.patienceMs` bounds goal *acceptance* only — once a goal
801
853
  * is accepted this call has already resolved; the job then runs as long
802
854
  * as it runs, observed via `subscribe`, never awaited. `options.timeoutMs`
803
855
  * (this SDK's own local wait for the acceptance reply) is derived from
804
856
  * `patienceMs` when left unset, and the combination `timeoutMs <
805
- * patienceMs` is refused with `invalid_option` rather than raced — see
806
- * `resolveLocalWaitMs` in `commands.ts`, D3a.
857
+ * patienceMs` is refused with `invalid_option` rather than raced.
807
858
  */
808
859
  invoke(robotId: string, slug: string, params: Record<string, unknown>, options?: InvokeOptions): Promise<Job>;
809
860
  /**
@@ -811,10 +862,9 @@ interface ActionsApi {
811
862
  * Resolves with the `Job` the cancel was actually sent to, or `null` if
812
863
  * nothing matched.
813
864
  *
814
- * **Two different requests, both legitimate (W6b):**
865
+ * **Two different requests, both legitimate:**
815
866
  * - `cancel(robotId, slug)` — no `jobId` — is the operator's stop button:
816
- * whatever is running on this slug, stop it. This is unchanged from
817
- * before W6b.
867
+ * whatever is running on this slug, stop it.
818
868
  * - `cancel(robotId, slug, jobId)` cancels **that** job specifically. If
819
869
  * it is not the one running, the platform answers `not_found` — this
820
870
  * never silently falls back to stopping whatever *is* running, because
@@ -830,7 +880,7 @@ interface ActionsApi {
830
880
  cancel(robotId: string, slug: string, jobId?: string | null, options?: SendCommandOptions): Promise<Job | null>;
831
881
  /**
832
882
  * Subscribes to the slug's job: state, feedback, progress and result, as
833
- * they happen. State is observed **by slug**, not by job id (§11.3) — this
883
+ * they happen. State is observed **by slug**, not by job id — this
834
884
  * is what makes late delivery after a reconnect and a second observer
835
885
  * watching the same job both work without special-casing either. Naming a
836
886
  * job to `cancel` does not change this: a slug is still a *place a job may
@@ -841,7 +891,9 @@ interface ActionsApi {
841
891
 
842
892
  /** An asset's bytes plus its declared media type — the shape `assets.get` answers with. */
843
893
  interface AssetBytes {
894
+ /** The asset's raw bytes, exactly as stored. */
844
895
  body: Uint8Array;
896
+ /** The declared media type, or `null` if the store did not record one. */
845
897
  mime: string | null;
846
898
  }
847
899
  /**
@@ -856,6 +908,7 @@ interface AssetBytes {
856
908
  * not only that one library.
857
909
  */
858
910
  type MeshLoaderDelegate = (path: string, manager: unknown, material: unknown, onComplete: (obj: unknown | null, err?: Error) => void) => void;
911
+ /** What `assets.createMeshLoader` accepts beyond the robot and the delegate. */
859
912
  interface CreateMeshLoaderOptions {
860
913
  /**
861
914
  * How long to wait for `delegate`'s `onComplete` before giving up.
@@ -867,12 +920,14 @@ interface CreateMeshLoaderOptions {
867
920
  timeoutMs?: number;
868
921
  }
869
922
  /**
870
- * three.js's own `LoadingManager.setURLModifier(callback)` shape (W7a, D2).
923
+ * three.js's own `LoadingManager.setURLModifier(callback)` shape the
924
+ * only thing `assets.prepareUrdfScene` needs from a three.js
925
+ * `LoadingManager`, so a caller passes theirs straight in.
871
926
  * Every load the manager oversees is routed through `callback` first — not
872
927
  * only the loader you handed the manager to, but every loader it constructs
873
928
  * internally on the same manager (`ColladaLoader`'s own `TextureLoader` for
874
929
  * a `.dae`'s `<init_from>` images, in particular). That is the one hook
875
- * that exists "one level up, for everything" (D2) where `createMeshLoader`'s
930
+ * that exists one level up, for everything, where `createMeshLoader`'s
876
931
  * per-loader `loadMeshCb` override does not reach: `TextureLoader` has no
877
932
  * override hook of its own.
878
933
  *
@@ -880,8 +935,10 @@ interface CreateMeshLoaderOptions {
880
935
  * or `urdf-loader`, same discipline as `MeshLoaderDelegate` above.
881
936
  */
882
937
  interface UrdfSceneManager {
938
+ /** Installs a callback every load through this manager is routed through first. */
883
939
  setURLModifier(callback: (url: string) => string): unknown;
884
940
  }
941
+ /** What `assets.prepareUrdfScene` accepts beyond the robot and the manager. */
885
942
  interface PrepareUrdfSceneOptions {
886
943
  /**
887
944
  * How many assets to fetch in parallel. Default 6 — a default that keeps
@@ -889,24 +946,23 @@ interface PrepareUrdfSceneOptions {
889
946
  * meshes), not a number with a sweep behind it. Pass your own if you have
890
947
  * a reason to.
891
948
  *
892
- * Must be a positive integer — `0` or negative throws `invalid_option`
893
- * rather than silently fetching nothing and returning a scene that
894
- * renders completely blank with no error to explain why.
949
+ * Must be a positive integer — `0` or negative rejects with
950
+ * `invalid_option` rather than silently fetching nothing and returning a
951
+ * scene that renders completely blank with no error to explain why.
895
952
  */
896
953
  concurrency?: number;
897
954
  /**
898
- * Cancels this call (register row 244, W7c) — a caller who navigates away
955
+ * Cancels this call — a caller who navigates away
899
956
  * or switches to a different robot mid-load can abort every in-flight
900
957
  * fetch this method has started, not merely stop it from starting new
901
958
  * ones. Checked before the first request; if it fires while a request is
902
- * already in progress, `HttpClient` forwards it straight to `fetch()`
903
- * (`http.ts`'s own `RequestOptions.signal`), so the connection itself is
904
- * torn down, not just abandoned by this SDK while it keeps running in the
905
- * background.
959
+ * already in progress, the SDK forwards it straight to `fetch()`, so the
960
+ * connection itself is torn down, not just abandoned by this SDK while it
961
+ * keeps running in the background.
906
962
  *
907
963
  * Every `blob:` URL already created before the abort is revoked before
908
964
  * this call rejects with `FleetlessError('aborted', ...)` — the same
909
- * guarantee a load that fails outright already had (D6): an aborted load
965
+ * guarantee a load that fails outright already had: an aborted load
910
966
  * must not leak what it had already fetched.
911
967
  *
912
968
  * `manager`'s URL modifier is only ever installed once every asset has
@@ -916,6 +972,10 @@ interface PrepareUrdfSceneOptions {
916
972
  */
917
973
  signal?: AbortSignal;
918
974
  }
975
+ /**
976
+ * What `assets.prepareUrdfScene` resolves with: the URDF text to parse, what
977
+ * the sync could not resolve, and the cleanup for everything it fetched.
978
+ */
919
979
  interface UrdfSceneResources {
920
980
  /**
921
981
  * The robot's URDF as raw text — `package://` URIs intact, not rewritten
@@ -926,24 +986,21 @@ interface UrdfSceneResources {
926
986
  urdfText: string;
927
987
  /**
928
988
  * The same entries `assets.list()`'s `urdf.missing` reports — verbatim,
929
- * not reduced to bare strings (W9b, DEF-081). Each carries `element`
989
+ * not reduced to bare strings. Each carries `element`
930
990
  * (`'mesh' | 'texture'`) alongside `uri`: before this, both kinds arrived
931
991
  * as an undifferentiated `string[]` and a caller could only ever say "N
932
- * meshes missing", wrongly, for a URDF whose gap was actually a texture
933
- * the same defect W9b fixed one layer up, in the console. Reducing this
934
- * field back to `string[]` here would throw the distinction away again at
992
+ * meshes missing", wrongly, for a URDF whose gap was actually a texture.
993
+ * Reducing this field back to `string[]` here would throw the distinction
994
+ * away again at
935
995
  * exactly the point a caller would render it. If you only need the URIs,
936
996
  * `missing.map(m => m.uri)`.
937
997
  *
938
- * **Top-level only — not a `.dae`'s internal references (Kassandra-W7a
939
- * review correction).** The cloud builds this list from the URDF text
998
+ * **Top-level only — not a `.dae`'s internal references.** The cloud
999
+ * builds this list from the URDF text
940
1000
  * alone (`<mesh>`/`<texture>` `filename` attributes), which is the only
941
1001
  * place it can see without parsing every `.dae` a sync touches; it never
942
- * has and never can include an internal `<init_from>` reference. An
943
- * earlier version of this comment claimed both wrong, and worth naming
944
- * as a correction rather than silently widening the sentence, since a
945
- * caller who trusted "both" to mean both would build a completeness check
946
- * against a list that structurally cannot report the second half. A
1002
+ * has and never can include an internal `<init_from>` reference. Do not
1003
+ * build a completeness check on it as though it covered both: a
947
1004
  * `.dae`-internal reference the sync could not resolve surfaces through
948
1005
  * the sync's own failure reporting instead, not here.
949
1006
  *
@@ -955,44 +1012,53 @@ interface UrdfSceneResources {
955
1012
  */
956
1013
  missing: UrdfCompleteness['missing'];
957
1014
  /**
958
- * Revokes every `blob:` URL this call created. Call once the scene has
959
- * finished loading (success or failure) or on unmount — safe to call more
960
- * than once.
1015
+ * Revokes every object URL this call created that carries bytes. Call
1016
+ * once the scene has finished loading (success or failure) or on unmount
1017
+ * — safe to call more than once.
1018
+ *
1019
+ * One object URL is deliberately kept: the shared zero-byte placeholder
1020
+ * every refused reference resolves to. It costs nothing to leave alive,
1021
+ * and leaving it is what lets this method stay simple — the installed URL
1022
+ * modifier goes on refusing an owned-but-gone reference correctly, rather
1023
+ * than falling back to the original string once the map is empty.
961
1024
  *
962
- * **Does not touch `manager`'s URL modifier (D7, Kassandra-W7a review).**
963
- * An earlier version reset it to the identity function here, which
964
- * silently reopened D4 the moment the same manager was used again — for a
965
- * second robot, or for anything else — before a later `prepareUrdfScene`
966
- * call happened to overwrite it. The installed modifier is left running,
1025
+ * **Does not touch `manager`'s URL modifier.** Resetting it to the
1026
+ * identity function here would reopen the very hole the modifier exists to
1027
+ * close, the moment the same manager was used again — for a second robot,
1028
+ * or for anything else — until a later `prepareUrdfScene` call happened to
1029
+ * overwrite it. The installed modifier is left running,
967
1030
  * and with this call's map now empty it already refuses anything it would
968
1031
  * have owned and passes through anything it would not have, correctly,
969
1032
  * on its own.
970
1033
  */
971
1034
  dispose(): void;
972
1035
  }
1036
+ /**
1037
+ * A robot's synced files, reachable as `client.assets`: its URDF, the meshes
1038
+ * and textures that URDF references, and the glue a three.js renderer needs
1039
+ * to fetch them with this client's credentials.
1040
+ */
973
1041
  interface AssetsApi {
974
1042
  /**
975
- * Every asset a robot has, plus whether its URDF is complete (spec §4.6).
1043
+ * Every asset a robot has, plus whether its URDF is complete.
976
1044
  * `urdf.missing` names the `package://` URIs the sync could not resolve —
977
- * the number alone ("2 Meshes fehlen") sends a developer looking through a
1045
+ * a count alone ("2 meshes missing") sends a developer looking through a
978
1046
  * workspace by hand, the URIs are what they can act on.
979
1047
  */
980
1048
  list(robotId: string): Promise<AssetListResponse>;
981
1049
  /**
982
1050
  * The status of one sync by id — for **reconnecting** to a sync already in
983
- * flight, not for starting one (W9b, DEF-147).
1051
+ * flight, not for starting one.
984
1052
  *
985
- * **Starting a sync stays out of this SDK, on purpose (spec §4.6):**
986
- * "übertragen wird ausschließlich auf explizite Anforderung des
987
- * Entwicklers über die Console" a developer action, Owner-tier, done
988
- * once, from the console. This method is a different thing: `list()`'s
1053
+ * **Starting a sync stays out of this SDK, on purpose.** Assets are
1054
+ * transferred only on a developer's explicit request from the console —
1055
+ * an Owner-tier action, done once. This method is a different thing:
1056
+ * `list()`'s
989
1057
  * `active_sync` (or a `busy` refusal's `assetSyncBusyDetails`) hands a
990
1058
  * caller a `sync_id` for a sync that is **already running**, and before
991
1059
  * this method existed there was no way for anything built on this SDK to
992
- * do anything with that id except throw it away. That was the actual
993
- * shape of DEF-147: the id was recoverable server-side the whole time
994
- * (`GET .../assets/sync/{id}` has existed since W7) — nothing reachable
995
- * from this SDK ever called it.
1060
+ * do anything with that id except throw it away, even though the sync was
1061
+ * readable server-side the whole time.
996
1062
  *
997
1063
  * A page reload is the case this exists for: whatever held the `sync_id`
998
1064
  * in memory is gone, `list()` (or a fresh `busy` refusal) hands it back,
@@ -1010,11 +1076,11 @@ interface AssetsApi {
1010
1076
  */
1011
1077
  urdf(robotId: string): Promise<string>;
1012
1078
  /**
1013
- * The mesh callback for `urdf-loader` (spec §4.6): an `<img>` tag and the
1014
- * default three.js loaders cannot set an `Authorization` header, and the
1015
- * platform deliberately has no signed URLs and no token in the query
1016
- * string (see `@fleetless/contracts` `assets.ts`), so every app would
1017
- * otherwise write this glue itself, and each one differently.
1079
+ * The mesh callback for `urdf-loader`: an `<img>` tag and the default
1080
+ * three.js loaders cannot set an `Authorization` header, and the platform
1081
+ * deliberately has no signed URLs and no token in the query string, so
1082
+ * every app would otherwise write this glue itself, and each one
1083
+ * differently.
1018
1084
  *
1019
1085
  * Returns a function with `loadMeshCb`'s own signature — assign it
1020
1086
  * directly:
@@ -1043,7 +1109,7 @@ interface AssetsApi {
1043
1109
  createMeshLoader(robotId: string, delegate: MeshLoaderDelegate, options?: CreateMeshLoaderOptions): MeshLoaderDelegate;
1044
1110
  /**
1045
1111
  * Authenticated loading for everything three.js fetches to render a
1046
- * textured robot — not only meshes (spec §4.6, W7a, D2). Installs
1112
+ * textured robot — not only meshes. Installs
1047
1113
  * `manager.setURLModifier` so **every** load `manager` oversees resolves
1048
1114
  * to a pre-fetched `blob:` URL: a top-level `<mesh>`, a `<material>`'s
1049
1115
  * `<texture>`, and an image a `.dae` references internally via
@@ -1065,9 +1131,8 @@ interface AssetsApi {
1065
1131
  * **Do not also install `createMeshLoader` on the same manager.** The two
1066
1132
  * consume different URDF sources — this method fetches the URDF's *raw*
1067
1133
  * bytes, `createMeshLoader` is meant to pair with `urdf()`'s
1068
- * cloud-rewritten text. **Not "double-fetches every mesh" a first
1069
- * version of this comment said that, and Momus-W7a's review traced it
1070
- * and found it wrong.** What actually happens is asymmetric breakage,
1134
+ * cloud-rewritten text. **It is not that every mesh gets fetched twice.**
1135
+ * What actually happens is asymmetric breakage,
1071
1136
  * whichever URDF text the combination ends up parsing: paired with
1072
1137
  * *this* method's raw text, `createMeshLoader` receives urdf-loader's
1073
1138
  * `resolvePath()` output (`/pkg/rel`) rather than an absolute Fleetless
@@ -1079,12 +1144,11 @@ interface AssetsApi {
1079
1144
  * debug the wrong symptom, which is worse than the original (already
1080
1145
  * wrong) warning being merely unhelpful.
1081
1146
  *
1082
- * **Enforced, not only documented (Momus-W7a review, via the team
1083
- * lead).** `createMeshLoader`'s returned callback checks whether the
1084
- * `manager` it is handed already has this method's URL modifier
1085
- * installed and fails loudly via `onComplete(null, err)` before ever
1086
- * touching the network, rather than relying on a developer having read
1087
- * this paragraph. See `managersWithPreparedUrdfScene`.
1147
+ * **Enforced, not only documented.** `createMeshLoader`'s returned
1148
+ * callback checks whether the `manager` it is handed already has this
1149
+ * method's URL modifier installed and fails loudly via
1150
+ * `onComplete(null, err)` before ever touching the network, rather than
1151
+ * relying on a developer having read this paragraph.
1088
1152
  *
1089
1153
  * **Why raw bytes, not `urdf()`'s rewritten text.** Both `urdf-loader`'s
1090
1154
  * default mesh loading and `ColladaLoader` compute the base path they use
@@ -1099,9 +1163,8 @@ interface AssetsApi {
1099
1163
  * in sync.
1100
1164
  *
1101
1165
  * **`urdf-loader` resolves `package://` itself, before any of this runs —
1102
- * a second resolution stage this method has to account for, found by
1103
- * Threepio-W7a running the recipe demo in a real browser rather than
1104
- * reading the source.** `URDFLoader.parse()`'s own `resolvePath()`
1166
+ * a second resolution stage this method has to account for, measured in a
1167
+ * real browser rather than read off the source.** `URDFLoader.parse()`'s own `resolvePath()`
1105
1168
  * rewrites `package://pkg/rel` using `this.packages` (default `''`) to
1106
1169
  * `/pkg/rel` — a root-relative URL — and *that* is what reaches
1107
1170
  * `loadMeshCb`/`ColladaLoader`/`manager.resolveURL()`, not the original
@@ -1121,29 +1184,29 @@ interface AssetsApi {
1121
1184
  * outside what this method can predict — see the ownership rule below
1122
1185
  * for what happens to that reference.
1123
1186
  *
1124
- * **This method only claims what it owns (D4, revised after Argus-W7a's
1125
- * review) — not every unmapped reference.** `manager` is frequently the
1187
+ * **This method only claims what it owns not every unmapped
1188
+ * reference.** `manager` is frequently the
1126
1189
  * caller's own scene-wide `LoadingManager`, shared for an HDRI, an
1127
1190
  * environment map, a font atlas, a ground texture — none of which have
1128
- * anything to do with this robot. A first version refused everything
1129
- * unmapped, which silently emptied every one of those the moment a
1130
- * caller shared their manager. So the rule is narrower: a `package://`
1191
+ * anything to do with this robot. Refusing everything unmapped would
1192
+ * silently empty every one of those the moment a caller shares their
1193
+ * manager. So the rule is narrower: a `package://`
1131
1194
  * reference, or a root-relative path whose leading segment names a ROS
1132
1195
  * package this robot's assets (or `missing`) actually mention, is this
1133
1196
  * method's to resolve or refuse; an unmapped one falls back to the
1134
- * normalized form (D5, below) and then to a shared, inert, page-local
1197
+ * normalized form (below) and then to a shared, inert, page-local
1135
1198
  * `blob:` URL — never the original string, so a hostile URDF naming an
1136
1199
  * unsynced or off-namespace reference still cannot make three.js touch
1137
1200
  * the network for it. Anything else — not in that namespace — is left
1138
1201
  * completely alone, **except** an absolute `http(s)` URL, which is
1139
1202
  * refused regardless of namespace: the one case this method cannot leave
1140
1203
  * ambiguous, because a hostile URDF naming an attacker's host directly
1141
- * (bypassing `package://` entirely) is exactly what D4 exists to close,
1142
- * and three.js would otherwise fetch it for real, off-origin, the moment
1204
+ * (bypassing `package://` entirely) is exactly what this rule exists to
1205
+ * close, and three.js would otherwise fetch it for real, off-origin, the moment
1143
1206
  * the direct and namespace checks both miss.
1144
1207
  *
1145
1208
  * **A `.dae`'s own internal reference gets a second-chance, normalized
1146
- * lookup (D5, Momus-W7a review, reproduced in a real browser).** three.js
1209
+ * lookup, reproduced in a real browser.** three.js
1147
1210
  * builds the request for one by plain string concatenation — no `..`/`.`
1148
1211
  * collapsing — while `asset.name` carries the *normalized* tail
1149
1212
  * (`@fleetless/contracts`' naming rule). So `../textures/skin.png` or
@@ -1154,17 +1217,17 @@ interface AssetsApi {
1154
1217
  * verbatim and `resolvePath()` rewrites it by the same unnormalized
1155
1218
  * concatenation on both sides, so the direct key already matches.
1156
1219
  *
1157
- * **`dispose()` does not undo any of this (D7, Kassandra-W7a review).**
1158
- * See its own doc comment on `UrdfSceneResources`.
1220
+ * **`dispose()` does not undo any of this.** See its own doc comment on
1221
+ * `UrdfSceneResources`.
1159
1222
  *
1160
1223
  * **Pre-fetch is unavoidable**, not merely a choice: a URL modifier
1161
1224
  * cannot be asynchronous, so every asset it might be asked for has to
1162
1225
  * already be a `blob:` URL before `URDFLoader.parse` runs. Bounded by
1163
1226
  * `options.concurrency` (default 6) and scoped to only `kind: 'mesh'` and
1164
- * `kind: 'texture'` assets — which is already "what the URDF references"
1165
- * (`@fleetless/contracts`' `rest.ts`: a re-sync reconciles, so assets the
1166
- * current URDF no longer references stop belonging to the robot), not an
1167
- * unbounded fetch of everything the robot has ever had.
1227
+ * `kind: 'texture'` assets — which is already "what the URDF references",
1228
+ * since a re-sync reconciles and assets the current URDF no longer
1229
+ * references stop belonging to the robot. Not an unbounded fetch of
1230
+ * everything the robot has ever had.
1168
1231
  */
1169
1232
  prepareUrdfScene(robotId: string, manager: UrdfSceneManager, options?: PrepareUrdfSceneOptions): Promise<UrdfSceneResources>;
1170
1233
  }
@@ -1182,35 +1245,56 @@ type StoredSession = SessionTokens;
1182
1245
  * assumes a browser, or any storage, exists.
1183
1246
  */
1184
1247
  interface TokenStore {
1248
+ /**
1249
+ * Returns the stored session, or `null` when nobody is logged in. May be
1250
+ * async, so a store backed by a native keystore or an IndexedDB read
1251
+ * works without a synchronous cache in front of it.
1252
+ */
1185
1253
  load(): StoredSession | null | Promise<StoredSession | null>;
1254
+ /**
1255
+ * Writes the session, or clears it when passed `null`. Called after a
1256
+ * login, after every silent refresh, and on logout — so an implementation
1257
+ * that persists must expect to be called often, not once.
1258
+ */
1186
1259
  save(session: StoredSession | null): void | Promise<void>;
1187
1260
  }
1188
1261
  /** The default store: works out of the box, forgets the session on reload. */
1189
1262
  declare class InMemoryTokenStore implements TokenStore {
1190
1263
  #private;
1264
+ /** Creates an empty store. Nothing is loaded from anywhere — a client built with it starts logged out. */
1265
+ constructor();
1266
+ /** Returns the session held in memory, or `null` if there is none. */
1191
1267
  load(): StoredSession | null;
1268
+ /** Replaces the session held in memory; `null` clears it. */
1192
1269
  save(session: StoredSession | null): void;
1193
1270
  }
1194
1271
 
1195
- /** `beginHostedLogin()`'s input (spec §3.4, §17, W7b). */
1272
+ /**
1273
+ * `beginHostedLogin()`'s input: the OAuth client the developer registered
1274
+ * for this app, where the browser should come back to, and optionally what
1275
+ * the resulting token should be usable against.
1276
+ */
1196
1277
  interface BeginHostedLoginOptions {
1197
1278
  /**
1198
1279
  * The opaque `client_id` issued when the developer registered this app's
1199
1280
  * OAuth client (console, App Settings). **Never `appIdentifier`** — they
1200
- * are deliberately different identifiers (`contracts/src/oauth.ts`,
1201
- * `oauthClient`'s doc comment: "the `client_id` on the wire opaque, and
1202
- * not the app identifier").
1281
+ * are deliberately different identifiers — the contracts call this "the
1282
+ * `client_id` on the wire: opaque, and not the app identifier".
1203
1283
  */
1204
1284
  clientId: string;
1205
1285
  /**
1206
1286
  * Must be registered, byte-for-byte, as one of that client's
1207
1287
  * `redirect_uris` — matching at the server is exact-string, never a
1208
- * prefix (`contracts/src/oauth.ts`, `redirectUri`'s doc comment).
1288
+ * prefix (the contracts' `redirectUri` doc comment says why).
1209
1289
  */
1210
1290
  redirectUri: string;
1291
+ /**
1292
+ * The OAuth scopes to request, space-separated. Omit it to get the
1293
+ * client's registered default, which is what an ordinary app login wants.
1294
+ */
1211
1295
  scope?: string;
1212
1296
  /**
1213
- * RFC 8707 audience binding (register row, W7b): the resource this
1297
+ * RFC 8707 audience binding: the resource this
1214
1298
  * session's token should be usable against. **Omit it for an ordinary app
1215
1299
  * login.** A token with no `resource` carries no `aud` and works
1216
1300
  * unrestricted against this app's own REST surface exactly as it always
@@ -1220,7 +1304,7 @@ interface BeginHostedLoginOptions {
1220
1304
  *
1221
1305
  * **`/oauth/authorize` accepts exactly two shapes** — anything else is
1222
1306
  * refused with `invalid_target` on the redirect back, before a code is
1223
- * ever issued (`cloud/src/routes/oauth.ts`, `isKnownResource`):
1307
+ * ever issued (the cloud's own known-resource check):
1224
1308
  *
1225
1309
  * - `<base>/mcp-stub/resource` — the global OAuth resource stub, matched
1226
1310
  * byte-for-byte.
@@ -1230,21 +1314,21 @@ interface BeginHostedLoginOptions {
1230
1314
  * be the one this `clientId` is registered to, so a client on app A can
1231
1315
  * never be minted a token whose `aud` names app B.
1232
1316
  *
1233
- * Both are served and validated by
1234
- * `cloud/src/routes/oauth-resource-stub.ts`, whose validator refuses a
1235
- * token with **no** `aud` exactly as hard as one with the wrong `aud` —
1317
+ * Both are served and validated by the platform's resource stub, whose
1318
+ * validator refuses a token with **no** `aud` exactly as hard as one with
1319
+ * the wrong `aud` —
1236
1320
  * "unscoped" must never read as "for me" — comparing by equality, never
1237
1321
  * by prefix.
1238
1322
  *
1239
1323
  * **`<base>/mcp/<app_identifier>` is not a resource any more.** There is
1240
1324
  * no per-app MCP endpoint: the cloud registers one central, non-parametric
1241
- * `POST /mcp` (`cloud/src/routes/mcp.ts`, contracts'
1242
- * `MCP_ENDPOINT_PATH`), and the `/oauth/authorize` branch that used to
1325
+ * `POST /mcp` (the contracts' `MCP_ENDPOINT_PATH`), and the
1326
+ * `/oauth/authorize` branch that used to
1243
1327
  * accept a `/mcp/<app>` resource was deleted along with the app-level
1244
1328
  * `mcp_enabled` flag. Asking for one now yields `invalid_target` for
1245
1329
  * every app, **including your own**. The central MCP endpoint has its own
1246
- * OAuth flow (`cloud/src/routes/mcp-oauth.ts`) which this SDK's hosted
1247
- * login does not drive — `beginHostedLogin` always targets
1330
+ * OAuth flow, which this SDK's hosted login does not drive —
1331
+ * `beginHostedLogin` always targets
1248
1332
  * `OAUTH_PATHS.authorize`.
1249
1333
  *
1250
1334
  * Whatever you name here is re-checked at the token exchange: it must
@@ -1262,8 +1346,8 @@ interface HostedLoginRequest {
1262
1346
  * both back into `completeHostedLogin`. **This SDK does not persist them
1263
1347
  * for you.** The redirect back to `redirectUri` is a fresh page load for a
1264
1348
  * browser app — nothing kept in this SDK's own memory survives it (the
1265
- * same reasoning `client.ts` states for `TokenStore`: "the SDK itself
1266
- * never assumes a browser, or any storage, exists"). An in-memory default
1349
+ * same reasoning `TokenStore` states: the SDK itself never assumes a
1350
+ * browser, or any storage, exists). An in-memory default
1267
1351
  * here would not be merely suboptimal, it would be broken for the primary
1268
1352
  * use case while looking like it worked for anything that never actually
1269
1353
  * navigates away. `sessionStorage`, a signed cookie, or a plain variable
@@ -1271,6 +1355,11 @@ interface HostedLoginRequest {
1271
1355
  * the caller's.
1272
1356
  */
1273
1357
  state: string;
1358
+ /**
1359
+ * The PKCE code verifier for this attempt. Persist it exactly as
1360
+ * `state` above and pass it back to `completeHostedLogin` — it is what
1361
+ * proves the code exchange comes from the client that started the flow.
1362
+ */
1274
1363
  codeVerifier: string;
1275
1364
  }
1276
1365
  /** `completeHostedLogin()`'s input — the redirect back, plus what `beginHostedLogin` returned for this same attempt. */
@@ -1281,13 +1370,14 @@ interface CompleteHostedLoginOptions {
1281
1370
  state: string;
1282
1371
  /**
1283
1372
  * The `state` this attempt's `beginHostedLogin` returned. Checked against
1284
- * `state` above **before any network call** — RFC 6749 §10.12's whole
1285
- * point is that a caller must not complete an authorization response it
1286
- * did not itself request.
1373
+ * `state` above **before any network call** — the whole point of RFC
1374
+ * 6749 section 10.12 is that a caller must not complete an authorization
1375
+ * response it did not itself request.
1287
1376
  */
1288
1377
  expectedState: string;
1289
1378
  /** The `codeVerifier` this attempt's `beginHostedLogin` returned. */
1290
1379
  codeVerifier: string;
1380
+ /** Must be the exact same `client_id` passed to `beginHostedLogin`. */
1291
1381
  clientId: string;
1292
1382
  /** Must be the exact same string passed to `beginHostedLogin`. */
1293
1383
  redirectUri: string;
@@ -1296,22 +1386,31 @@ interface CompleteHostedLoginOptions {
1296
1386
  * Resending it here is not what binds the audience — the server already
1297
1387
  * bound `resource` to the authorization code at `/oauth/authorize` and
1298
1388
  * mints `aud` from that stored value regardless of what this call sends —
1299
- * but RFC 8707 §2 expects a client to name the resource at both steps,
1389
+ * but RFC 8707 section 2 expects a client to name the resource at both steps,
1300
1390
  * and the cloud rejects a *mismatched* resend outright (`invalid_target`).
1301
1391
  * Omit it here exactly when it was omitted at `beginHostedLogin`.
1302
1392
  */
1303
1393
  resource?: string;
1304
1394
  }
1305
1395
  /**
1306
- * What `logout()` resolves with (W9c, DEF-098) — five separable facts, not
1307
- * one nullable URL, mirroring the wire's `clientLogoutResponse`.
1396
+ * What `logout()` resolves with — five separable facts, not one nullable
1397
+ * URL, mirroring the wire's `clientLogoutResponse`.
1398
+ *
1399
+ * **`idp_logout` is `null` whenever the server never answered**, which
1400
+ * happens for two different reasons: there was no local session to ask
1401
+ * about, or the request itself failed. Either way there is nothing to
1402
+ * report — not even "not federated", because this client never learned that
1403
+ * either.
1308
1404
  *
1309
- * `idp_logout` is `null` exactly when `revoked` is `false`: the server call
1310
- * that would have produced it never got a chance to answer (no local
1311
- * session, or the request itself failed), so there is nothing to report —
1312
- * not even "not federated", because this client never learned that either.
1313
- * When `revoked` is `true` and a real response came back, `idp_logout` is
1314
- * one of:
1405
+ * `revoked` does not identify which of the two you got. It is `false` only
1406
+ * for the failed request; **a logout with no local session at all resolves
1407
+ * `{ revoked: true, idp_logout: null }`**, since nothing lingers
1408
+ * server-side and that counts as revoked. So `revoked: false` does imply
1409
+ * `idp_logout: null`, and the converse does not hold. Do not read the two
1410
+ * fields as one bit.
1411
+ *
1412
+ * When a real response did come back, `revoked` is `true` and `idp_logout`
1413
+ * is one of:
1315
1414
  *
1316
1415
  * - `{ status: 'redirect', url }` — send the browser here to end the
1317
1416
  * session at the IdP too. Nothing else in this SDK does that navigation
@@ -1334,27 +1433,61 @@ interface CompleteHostedLoginOptions {
1334
1433
  * isn't.
1335
1434
  *
1336
1435
  * **`session_unknown` is a different kind of nothing, and the reason it
1337
- * exists is a second logout** (W9 review). A double click, a repeated POST, an
1436
+ * exists is a second logout.** A double click, a repeated POST, an
1338
1437
  * app that logs out on unmount *and* on a route change: the last call wins,
1339
1438
  * and before this outcome existed it answered `not_federated` — a positive
1340
1439
  * claim about an IdP the server had never looked up. An app that treats it as
1341
1440
  * *"fully logged out"* skips a redirect the **first** call may well have
1342
1441
  * returned, and the user stays signed in at the IdP after clicking log out.
1343
1442
  *
1344
- * Note that `revoked` cannot help you here: it reports whether the HTTP call
1345
- * succeeded, not whether a session was found — so this case arrives as
1346
- * `{ revoked: true, idp_logout: { status: 'session_unknown' } }`, never as the
1347
- * `null` below.
1443
+ * Note that `revoked` cannot help you here either: it reports whether the
1444
+ * HTTP call succeeded, not whether a session was found — so this case
1445
+ * arrives as `{ revoked: true, idp_logout: { status: 'session_unknown' } }`,
1446
+ * never as `null`.
1348
1447
  */
1349
1448
  interface LogoutResult {
1449
+ /**
1450
+ * Whether the server-side revoke actually happened. It reports the fate
1451
+ * of the HTTP call, not whether a session was found — `false` means the
1452
+ * refresh family may still be alive even though this client has
1453
+ * forgotten it.
1454
+ */
1350
1455
  revoked: boolean;
1456
+ /**
1457
+ * What the server could say about the identity provider behind this
1458
+ * session, or `null` when there was no answer to report at all — either
1459
+ * because there was no local session to ask about, or because the request
1460
+ * failed. `null` is **not** the same as `not_federated`, which is a real
1461
+ * finding about a real session.
1462
+ *
1463
+ * When it is not `null` it is one of five statuses: `redirect` (with a
1464
+ * `url` to send the browser to, to end the session at the identity
1465
+ * provider too), `not_federated` (this session never came from one),
1466
+ * `unsupported_by_idp` (it did, and the provider offers no
1467
+ * RP-initiated logout), `hint_unavailable` (it did, the provider can end
1468
+ * the session, and Fleetless has nothing to ask it with), or
1469
+ * `session_unknown` (the server did not find this session, so it can say
1470
+ * nothing about a provider).
1471
+ *
1472
+ * **`unsupported_by_idp` and `hint_unavailable` both mean the identity
1473
+ * provider's session survives and this platform cannot end it.** Render
1474
+ * either one like `not_federated` and you report a session as fully ended
1475
+ * when it is not.
1476
+ */
1351
1477
  idp_logout: ClientLogoutResponse['idp_logout'] | null;
1352
1478
  }
1479
+ /**
1480
+ * Who the caller is, reachable as `client.auth`. A client built with a
1481
+ * `tokenStore` uses the full surface. One built with a `serverKey` already
1482
+ * has an identity and no user session, so **`me()` is the only method it
1483
+ * can call** — `login`, `beginHostedLogin`, `completeHostedLogin`,
1484
+ * `logout`, `changePassword` and `passwordResetUrl` all throw on one.
1485
+ */
1353
1486
  interface AuthApi {
1354
1487
  /** Exchanges email + password, and the client's configured app identifier, for a session. */
1355
1488
  login(email: string, password: string): Promise<void>;
1356
1489
  /**
1357
- * Starts the hosted login flow (spec §3.4, §17, W7b): a Fleetless-served
1490
+ * Starts the hosted login flow: a Fleetless-served
1358
1491
  * login page an app's end user is redirected to, with optional
1359
1492
  * per-app IdP federation. Builds the `/oauth/authorize` URL (Authorization
1360
1493
  * Code + PKCE, S256 only — OAuth 2.1 removes `plain`) and generates the
@@ -1386,12 +1519,12 @@ interface AuthApi {
1386
1519
  *
1387
1520
  * Once past that check and the exchange completes, `me()`, `logout()`,
1388
1521
  * `changePassword()` and silent refresh all behave identically afterwards,
1389
- * regardless of which flow the session started from. That is the actual
1390
- * content of §3.4's *"Beide Wege enden im selben Fleetless-Token"*: not
1391
- * merely that the bytes match, but that every existing code path treats
1392
- * the result the same way.
1522
+ * regardless of which flow the session started from. Both routes into a
1523
+ * session end at the same Fleetless token: not merely that the bytes
1524
+ * match, but that every existing code path treats the result the same
1525
+ * way.
1393
1526
  *
1394
- * The wire response is RFC 6749 §5.1's envelope (`token_type`, optional
1527
+ * The wire response is the envelope of RFC 6749 section 5.1 (`token_type`, optional
1395
1528
  * `scope`), not `sessionTokens` — this method normalizes one into the
1396
1529
  * other before storing. **Refresh needs no separate handling**: the cloud
1397
1530
  * mints these tokens through the same session mechanism `/api/client/login`
@@ -1403,7 +1536,7 @@ interface AuthApi {
1403
1536
  * Throws with the OAuth error code as `.code` (e.g. `invalid_grant` for an
1404
1537
  * expired or already-used `code`) if the exchange itself fails — a
1405
1538
  * different vocabulary from every other method on this interface, because
1406
- * `/oauth/token` answers in RFC 6749 §5.2's shape, not `apiError`.
1539
+ * `/oauth/token` answers in the shape of RFC 6749 section 5.2, not `apiError`.
1407
1540
  *
1408
1541
  * **Makes exactly one request to `/oauth/token` — never retried, no
1409
1542
  * timeout-and-resend, no internal concurrency of its own.** Stated
@@ -1412,16 +1545,16 @@ interface AuthApi {
1412
1545
  * while a first call is still in flight** (a plain "the first attempt
1413
1546
  * looked like it timed out, so retry" is exactly the shape this warns
1414
1547
  * against — it is not a defect in this method, since this method itself
1415
- * has nothing that could ever cause that). André's decision, 2026-08-18:
1416
- * the platform treats a second presentation of an authorization code as
1417
- * theft and revokes the whole token family it belongs to, deliberately,
1418
- * even though a plain double-submission looks identical on the wire —
1548
+ * has nothing that could ever cause that). The platform treats a second
1549
+ * presentation of an authorization code as theft and revokes the whole
1550
+ * token family it belongs to, deliberately, even though a plain
1551
+ * double-submission looks identical on the wire —
1419
1552
  * because the blast radius is bounded (only a caller already holding the
1420
1553
  * correct `code_verifier` and `client_id` can trigger it, so a merely
1421
1554
  * *sniffed* code cannot lock anyone out) and the alternative is a
1422
1555
  * narrower defence against a real theft.
1423
1556
  *
1424
- * **What actually happens if two requests race (measured, Argus-W7c):**
1557
+ * **What actually happens if two requests race, measured:**
1425
1558
  * one of the two receives `200` with a refresh token that the server has
1426
1559
  * already revoked. The access token in that same response keeps working
1427
1560
  * normally for the rest of its short TTL — nothing about the race is
@@ -1459,8 +1592,8 @@ interface AuthApi {
1459
1592
  * request without a fresh token), not a bug — but a kiosk or shared
1460
1593
  * workstation needs to know that number.
1461
1594
  *
1462
- * **Nor does it end a federated session at the identity provider (W9c,
1463
- * DEF-098).** Ending the Fleetless session and ending the IdP session are
1595
+ * **Nor does it end a federated session at the identity provider.**
1596
+ * Ending the Fleetless session and ending the IdP session are
1464
1597
  * two different things — see `LogoutResult.idp_logout`. This method does
1465
1598
  * not act on that information itself (no redirect, no fetch to the IdP);
1466
1599
  * it only reports what the server found, the same "this SDK stays thin"
@@ -1470,30 +1603,22 @@ interface AuthApi {
1470
1603
  * **`idp_logout` is `null` when there was no server answer to report** —
1471
1604
  * and that is *not* the same as `revoked === false`.
1472
1605
  *
1473
- * This block used to state the biconditional (*"`null` exactly when
1474
- * `revoked` is `false`"*), and **it is measurably wrong** (Argus-W9, W9
1475
- * review): calling `logout()` with **no local session** returns
1476
- * `{ revoked: true, idp_logout: null }`. The implementation says so six
1477
- * lines above itself — *"Nothing to revoke: no server-side session lingers,
1478
- * so this counts as revoked — but there is no IdP fact to report either"* —
1479
- * so the public docblock and the code contradicted each other in one file.
1480
- *
1481
- * That mattered more than a wrong sentence usually does, because **this is
1482
- * the rule the text tells a caller to branch on**. Someone who reads
1483
- * `revoked === true` and trusts the promise does not expect `null`.
1484
- * TypeScript catches it (the type is `| null`); a JavaScript caller does
1485
- * not.
1606
+ * Calling `logout()` with **no local session** returns
1607
+ * `{ revoked: true, idp_logout: null }`, so `null` is not the same as
1608
+ * `revoked === false` and the two fields are not one bit. TypeScript
1609
+ * catches a caller who forgets (the type is `| null`); a JavaScript caller
1610
+ * does not.
1486
1611
  *
1487
1612
  * So: **`null` means this client had nothing to send or the request never
1488
1613
  * answered.** It is not `not_federated`, which is a real finding about a
1489
1614
  * real session — conflating the two is the ambiguity this shape exists to
1490
- * remove, and it is still the reason to read both fields rather than one.
1615
+ * remove, and it is the reason to read both fields rather than one.
1491
1616
  */
1492
1617
  logout(): Promise<LogoutResult>;
1493
1618
  /** Who the caller turned out to be, without decoding a token client-side. */
1494
1619
  me(): Promise<ClientIdentity>;
1495
1620
  /**
1496
- * Changes the current end user's password (spec §3, W6c).
1621
+ * Changes the current end user's password.
1497
1622
  *
1498
1623
  * `currentPassword` is required by the server even though the session
1499
1624
  * already proves identity — it is what stops a stolen *session* from
@@ -1531,7 +1656,7 @@ interface AuthApi {
1531
1656
  }
1532
1657
 
1533
1658
  /**
1534
- * The snapshot's metadata alone, without the bytes (spec §10). All fields
1659
+ * The snapshot's metadata alone, without the bytes. All fields
1535
1660
  * are `null` together when nothing has been captured yet for this camera —
1536
1661
  * a fresh configuration before the first `snapshot_interval_ms` elapses, say
1537
1662
  * — which is a state, not a failure: the wire answers it with
@@ -1546,36 +1671,49 @@ interface AuthApi {
1546
1671
  * `SNAPSHOT_HEADERS`'s doc comment in `@fleetless/contracts`).
1547
1672
  */
1548
1673
  interface CameraSnapshotMeta {
1674
+ /** The image's media type, e.g. `image/jpeg`. */
1549
1675
  mime: string | null;
1676
+ /** The image's width in pixels. */
1550
1677
  width: number | null;
1678
+ /** The image's height in pixels. */
1551
1679
  height: number | null;
1552
- /** The bridge's capture time (§6.3). */
1680
+ /** The bridge's capture time, in unix milliseconds — when the frame was taken, not when it was served. */
1553
1681
  timestamp_ms: number | null;
1682
+ /** How long the cloud has held this frame, in milliseconds. The one figure to read for freshness. */
1554
1683
  age_ms: number | null;
1555
1684
  }
1556
- /** One snapshot read: the image bytes plus everything needed to state how old they are (spec §10). */
1685
+ /** One snapshot read: the image bytes plus everything needed to state how old they are. */
1557
1686
  interface CameraSnapshot extends CameraSnapshotMeta {
1687
+ /** The encoded image, or `null` when nothing has been captured yet. */
1558
1688
  image: Uint8Array | null;
1559
1689
  }
1560
1690
  /**
1561
- * What a LiveKit client needs to join, plus the means to leave (spec §10,
1562
- * §14.3: "Kamera (Snapshot-URL + LiveKit-Track-Handle)"). Hand `url`/`token`
1563
- * straight to a LiveKit client SDK (e.g. `Room.connect(url, token)`) — this
1564
- * SDK stops there on purpose: no video widget, no teleop-style helper
1565
- * (§14.3 keeps it thin).
1691
+ * What a LiveKit client needs to join, plus the means to leave. Returned by
1692
+ * `cameras.live`. Hand `url` and `token` straight to a LiveKit client SDK
1693
+ * (e.g. `Room.connect(url, token)`) — this SDK stops there on purpose: no
1694
+ * video widget and no teleop-style helper, so that the app owns how the
1695
+ * video is presented.
1566
1696
  */
1567
1697
  interface CameraLiveSession {
1568
1698
  /**
1569
- * This viewer's own hold (W6b) — what `release()` releases, and the only
1699
+ * This viewer's own hold — what `release()` releases, and the only
1570
1700
  * thing distinguishing this session from every other tab of the same
1571
- * identity watching the same camera. Not previously addressable: a `DELETE`
1572
- * with no id released **all** of this identity's holds on the slug, so one
1573
- * tab closing stopped the robot for every other tab too. See `release()`'s
1574
- * doc comment for what changed and what did not.
1701
+ * identity watching the same camera.
1575
1702
  */
1576
1703
  session_id: string;
1704
+ /** The LiveKit server URL to connect to. */
1577
1705
  url: string;
1706
+ /** The LiveKit room this session joins. */
1578
1707
  room: string;
1708
+ /**
1709
+ * The LiveKit access token for this session. It is checked when the
1710
+ * participant connects and not again afterwards, so it bounds *joining*,
1711
+ * not the session: a viewer who has already joined keeps receiving video
1712
+ * past `expires_at`. What ends a joined session is `release()` together
1713
+ * with disconnecting the room, the cloud reconciling the hold away
1714
+ * against LiveKit's real participants, or a revocation — a membership,
1715
+ * role or key change — kicking the participant out.
1716
+ */
1579
1717
  token: string;
1580
1718
  /**
1581
1719
  * When this token can no longer be used to **join** — not when an
@@ -1594,14 +1732,10 @@ interface CameraLiveSession {
1594
1732
  expires_at: string;
1595
1733
  /**
1596
1734
  * Tells the cloud this viewer no longer wants to hold the camera live —
1597
- * **this** hold, addressed by `session_id` (W6b), and no other tab's.
1598
- *
1599
- * Before W6b, `DELETE` carried no id and released every hold this identity
1600
- * had on the slug — so one tab's `release()` (or its unmount cleanup)
1601
- * stopped the robot out from under every other tab of the same logged-in
1602
- * user, which kept rendering a frozen frame because a LiveKit token is
1603
- * checked at join and never again. Each `CameraLiveSession` now releases
1604
- * only the hold it itself took.
1735
+ * **this** hold, addressed by `session_id`, and no other tab's. Each
1736
+ * `CameraLiveSession` releases only the hold it itself took, so one tab's
1737
+ * cleanup never stops the robot out from under another tab of the same
1738
+ * logged-in user.
1605
1739
  *
1606
1740
  * **This alone does not stop the stream.** The cloud makes LiveKit room
1607
1741
  * participation the authoritative refcount, not this call — precisely
@@ -1622,7 +1756,7 @@ interface CameraLiveSession {
1622
1756
  *
1623
1757
  * **A failed DELETE here is not observable anywhere** — not as a
1624
1758
  * rejection, not as a realtime event, not as a field on this object. This
1625
- * is a deliberate decision, not an oversight (W6b review): the only
1759
+ * is a deliberate decision, not an oversight: the only
1626
1760
  * consumer of that information would be code deciding whether to retry,
1627
1761
  * and the backstop this comment already describes — the cloud's own
1628
1762
  * LiveKit-participation reconciliation — makes a retry unnecessary for
@@ -1632,13 +1766,18 @@ interface CameraLiveSession {
1632
1766
  */
1633
1767
  release(): Promise<void>;
1634
1768
  }
1769
+ /**
1770
+ * A robot's cameras, reachable as `client.cameras`: what exists, the latest
1771
+ * still frame, and a live video session. All of it is REST — no realtime
1772
+ * channel is involved.
1773
+ */
1635
1774
  interface CamerasApi {
1636
- /** Every camera exposed on this robot (spec §11.2's per-robot descriptor list, extended to the camera kind). */
1775
+ /** Every camera exposed on this robot, as descriptors — the same per-robot list the other kinds use. */
1637
1776
  list(robotId: string): Promise<CameraDescriptor[]>;
1638
1777
  /**
1639
1778
  * The current snapshot: image bytes plus its age. Independent of `live` —
1640
1779
  * a snapshot keeps updating on `snapshot_interval_ms` whether or not
1641
- * anyone is watching live (§10), and keeps being served, with a growing
1780
+ * anyone is watching live, and keeps being served, with a growing
1642
1781
  * age, even while the bridge is offline.
1643
1782
  */
1644
1783
  snapshot(robotId: string, slug: string): Promise<CameraSnapshot>;
@@ -1650,30 +1789,44 @@ interface CamerasApi {
1650
1789
  */
1651
1790
  snapshotMeta(robotId: string, slug: string): Promise<CameraSnapshotMeta>;
1652
1791
  /**
1653
- * Takes a refcounted hold on this camera's live stream (spec §10): the
1792
+ * Takes a refcounted hold on this camera's live stream: the
1654
1793
  * first `live()` on a slug starts the robot publishing, the last viewer
1655
1794
  * leaving stops it. Deliberately not deduplicated locally across multiple
1656
1795
  * `live()` calls for the same `(robotId, slug)` — unlike a datapoint
1657
1796
  * subscription, each call needs its own distinct LiveKit participant, so
1658
- * a local counter here would just be the same shared-count bug
1659
- * `slug-subscriptions.ts` fixed, self-inflicted on a resource the cloud
1660
- * already counts correctly.
1797
+ * a local counter here would just be the same shared-count bug the
1798
+ * subscription layer already fixed, self-inflicted on a resource the
1799
+ * cloud counts correctly on its own.
1661
1800
  */
1662
1801
  live(robotId: string, slug: string): Promise<CameraLiveSession>;
1663
1802
  }
1664
1803
 
1804
+ /**
1805
+ * The callbacks `datapoints.subscribe` reports through: one for values, one
1806
+ * for a refusal. `onEvent` fires immediately with the current value and
1807
+ * again on every change.
1808
+ */
1665
1809
  interface DatapointSubscriptionHandlers {
1666
1810
  /** Called with the current value on subscribe, then again on every change. */
1667
1811
  onEvent(event: DatapointEvent): void;
1668
- /** Called once if the subscription is refused (§11.5: e.g. `forbidden`, `unknown_datapoint`). */
1812
+ /** Called once if the subscription is refused, e.g. `forbidden` or `unknown_datapoint`. */
1669
1813
  onError?(error: FleetlessError): void;
1670
1814
  }
1815
+ /** A live datapoint subscription, returned by `datapoints.subscribe`. */
1671
1816
  interface DatapointSubscription {
1672
- /** Stops the subscription and, if the channel is currently connected, tells the server. */
1817
+ /**
1818
+ * Stops this subscription. The `unsubscribe` frame reaches the server only
1819
+ * when this was the **last** holder of the robot and slug pair, and only if
1820
+ * the channel is connected — subscriptions are reference-counted across
1821
+ * kinds, so releasing this one while a second `datapoints.subscribe`, an
1822
+ * `actions.subscribe` or an in-flight `services.call` still holds the same
1823
+ * pair leaves that one's stream running untouched. Safe to call more than
1824
+ * once.
1825
+ */
1673
1826
  unsubscribe(): void;
1674
1827
  }
1675
1828
  /**
1676
- * Window aggregation for `history` (spec §8). `window` and `agg` always
1829
+ * Window aggregation for `datapoints.history`. `window` and `agg` always
1677
1830
  * travel together on the wire — the cloud refuses one without the other
1678
1831
  * rather than defaulting either, since a silently chosen aggregation is a
1679
1832
  * chart that lies quietly — so they live in one object here instead of two
@@ -1685,10 +1838,16 @@ interface DatapointSubscription {
1685
1838
  interface HistoryAggregation {
1686
1839
  /** Bucket width, e.g. `10s`, `1m`. */
1687
1840
  window: string;
1841
+ /** How to reduce each bucket's samples to one number. */
1688
1842
  agg: 'min' | 'max' | 'avg';
1689
- /** A numeric field inside an object value, e.g. `pose.x` (§4.4 paths). Only meaningful when the datapoint's own value is not itself a number. */
1843
+ /** A numeric field inside an object value, e.g. `pose.x`. Only meaningful when the datapoint's own value is not itself a number. */
1690
1844
  field?: string;
1691
1845
  }
1846
+ /**
1847
+ * The window `datapoints.history` reads, and whether it comes back as raw
1848
+ * samples or as aggregated buckets. `aggregate` is what decides which of
1849
+ * the two responses you get.
1850
+ */
1692
1851
  interface HistoryOptions {
1693
1852
  /**
1694
1853
  * `now-30s` / `now-5m` / `now-1h`, or absolute unix milliseconds — as a
@@ -1701,11 +1860,22 @@ interface HistoryOptions {
1701
1860
  from: string;
1702
1861
  /** Same two forms as `from`. Defaults to now. */
1703
1862
  to?: string;
1863
+ /** The most rows to return. The platform applies its own ceiling regardless. */
1704
1864
  limit?: number;
1705
1865
  /** Present: the result is aggregated buckets. Absent: raw samples. */
1706
1866
  aggregate?: HistoryAggregation;
1707
1867
  }
1868
+ /**
1869
+ * A robot's exposed values, reachable as `client.datapoints`: the latest
1870
+ * one, a live subscription to it, and — for a datapoint configured with
1871
+ * retention — its recorded history.
1872
+ */
1708
1873
  interface DatapointsApi {
1874
+ /**
1875
+ * Reads the datapoint's latest value over REST, once. It carries the
1876
+ * bridge's own capture time, so a caller can tell a fresh value from a
1877
+ * stale one without a subscription.
1878
+ */
1709
1879
  get(robotId: string, slug: string): Promise<DatapointValue>;
1710
1880
  /**
1711
1881
  * Subscribes over the realtime channel. Reconnect and re-authentication
@@ -1718,25 +1888,27 @@ interface DatapointsApi {
1718
1888
  * other — the `unsubscribe` frame is sent only when the last subscriber
1719
1889
  * on that pair goes away. This matters in practice: two widgets showing
1720
1890
  * the same battery value, or a component mounted twice under React
1721
- * StrictMode, both subscribe to the same key. The count itself lives in
1722
- * `slug-subscriptions.ts`, shared with `actions.subscribe`/`services.call`
1723
- * — a slug is one namespace across kinds, and so is its subscription.
1891
+ * StrictMode, both subscribe to the same key. That count is shared with
1892
+ * `actions.subscribe` and `services.call` — a slug is one namespace across
1893
+ * kinds, and so is its subscription.
1724
1894
  */
1725
1895
  subscribe(robotId: string, slug: string, handlers: DatapointSubscriptionHandlers): DatapointSubscription;
1726
1896
  /**
1727
- * Reads recorded history for a `retention: true` datapoint (spec §8) over
1728
- * REST — no realtime channel involved, the same way `cameras.snapshot`
1729
- * isn't. Returns a **discriminated result**: passing `aggregate` gets you
1730
- * back `HistoryBucketsResponse` (`kind: 'buckets'`), leaving it out gets
1731
- * you `HistorySamplesResponse` (`kind: 'samples'`) two overloads so a
1732
- * caller who already knows which one they asked for isn't forced to
1733
- * narrow something they determined themselves. `kind` still carries the
1734
- * same information on both, so code that holds the result dynamically
1735
- * (e.g. read from a variable typed as the union) can still branch on it.
1897
+ * Reads recorded history for a `retention: true` datapoint over REST — no
1898
+ * realtime channel involved, the same way `cameras.snapshot` isn't. **This
1899
+ * overload is the aggregated one:** `aggregate` is given, so it resolves
1900
+ * with `HistoryBucketsResponse` (`kind: 'buckets'`) one row per window,
1901
+ * reduced by `aggregate.agg`. Leave `aggregate` out and the other overload
1902
+ * gives you raw samples instead.
1903
+ *
1904
+ * Two overloads rather than one union so a caller who already knows which
1905
+ * they asked for isn't forced to narrow something they determined
1906
+ * themselves. `kind` still carries the same information on both, so code
1907
+ * holding the result dynamically can still branch on it.
1736
1908
  *
1737
1909
  * **Rejects, does not silently empty out, two specific refusals** —
1738
1910
  * unlike `cameras.snapshot`'s absorption of `no_snapshot_yet` into a null
1739
- * read, these two must reach the caller as thrown `FleetlessError`s:
1911
+ * read, these two must reach the caller as a rejected `FleetlessError`:
1740
1912
  * - `not_recorded` — the slug exists and is granted, but is configured
1741
1913
  * live-only. An empty result here would look exactly like "recorded,
1742
1914
  * but nothing in this window", and the two need opposite fixes: turn
@@ -1747,20 +1919,34 @@ interface DatapointsApi {
1747
1919
  history(robotId: string, slug: string, options: HistoryOptions & {
1748
1920
  aggregate: HistoryAggregation;
1749
1921
  }): Promise<HistoryBucketsResponse>;
1922
+ /**
1923
+ * The same read without `aggregate`: resolves with
1924
+ * `HistorySamplesResponse` (`kind: 'samples'`), every recorded sample in
1925
+ * the window as a `timestamp_ms` and a `value`. `timestamp_ms` is the
1926
+ * bridge's own capture time, the same instant the live value carried, so a
1927
+ * recorded point and a live one sit on one axis without apology.
1928
+ *
1929
+ * **Read `truncated`.** The platform caps how much one read returns, by
1930
+ * row count or by bytes, and `truncated_by` says which. A short array that
1931
+ * does not admit it is indistinguishable from a quiet period, and the two
1932
+ * lead to opposite conclusions. Refuses `not_recorded` the same way the
1933
+ * aggregated overload does.
1934
+ */
1750
1935
  history(robotId: string, slug: string, options: HistoryOptions & {
1751
1936
  aggregate?: undefined;
1752
1937
  }): Promise<HistorySamplesResponse>;
1753
1938
  }
1754
1939
 
1755
1940
  /**
1756
- * An end user's own consent grants (spec §3.4/§17, W9c, DEF-099) — every
1941
+ * An end user's own consent grants, reachable as `client.grants` — every
1757
1942
  * client this identity has ever authorized, and the means to take one back
1758
1943
  * without touching any of the others.
1759
1944
  *
1760
1945
  * **End-user only.** A server key acts with the app's own full rights and
1761
1946
  * never went through a consent screen itself — there is no "self" here for
1762
- * it to list or revoke, same reasoning as `auth.login`/`auth.register` on a
1763
- * `serverKey` client.
1947
+ * it to list or revoke, same reasoning as `auth.login` on a `serverKey`
1948
+ * client. An end user reaches a group by invitation and signs in through
1949
+ * `auth.login` or the hosted login; a server key never does either.
1764
1950
  */
1765
1951
  interface GrantsApi {
1766
1952
  /**
@@ -1769,9 +1955,9 @@ interface GrantsApi {
1769
1955
  * `role_name`) — the id is what `revoke()` addresses, but a person
1770
1956
  * deciding whether to revoke something needs to *recognise* it first, and
1771
1957
  * an id is not recognisable. **`client_name` is not trusted** — a
1772
- * self-registered client chooses its own display name (W7c already
1773
- * measured what that buys: one called itself "Fleetless Official
1774
- * Helper") — do not render it as if Fleetless vouched for it.
1958
+ * self-registered client chooses its own display name, and one has
1959
+ * already been measured calling itself "Fleetless Official Helper" — do
1960
+ * not render it as if Fleetless vouched for it.
1775
1961
  */
1776
1962
  list(): Promise<ConsentGrantSummary[]>;
1777
1963
  /**
@@ -1786,9 +1972,8 @@ interface GrantsApi {
1786
1972
  * about access tokens, which are stateless and short-lived by design.
1787
1973
  *
1788
1974
  * **What actually happens to an already-issued access token is stronger
1789
- * than that count implies, and it was measured, not assumed (W9c gate
1790
- * step 1, 2026-08-19, end to end through this SDK against a real
1791
- * cloud).** It does not simply expire on its own: the cloud re-checks
1975
+ * than that count implies.** It does not simply expire on its own: the
1976
+ * cloud re-checks
1792
1977
  * every request for a revoked grant, so **any** currently-valid access
1793
1978
  * token minted through this same client's OAuth flow for this end user
1794
1979
  * — not only the one used to call `revoke()` — answers `401
@@ -1804,18 +1989,22 @@ interface GrantsApi {
1804
1989
  revoke(clientId: string): Promise<ConsentRevokeResponse>;
1805
1990
  }
1806
1991
 
1992
+ /**
1993
+ * Robot-wide job reads, reachable as `client.jobs`. Everything here is
1994
+ * addressed by robot rather than by slug, which is what `actions` and
1995
+ * `services` cannot do.
1996
+ */
1807
1997
  interface JobsApi {
1808
1998
  /**
1809
- * Every job the platform currently believes this robot has — `GET
1810
- * /api/robots/:id/jobs` (W6b, contracts `robotJobsResponse` doc comment).
1999
+ * Every job the platform currently believes this robot has.
1811
2000
  *
1812
2001
  * `actions.subscribe`/`services.call` and `GET /jobs/:slug` (the per-slug
1813
2002
  * route those build on) all require already knowing the slug. That is not
1814
2003
  * always true: a reconnecting bridge can name a job the cloud only
1815
2004
  * *adopted*, and a configuration change can leave a job on a slug the
1816
2005
  * published document no longer contains. Both are jobs no slug can name,
1817
- * which is exactly what this method is for (register row 2k) an app
1818
- * developer had no way to reach them before this.
2006
+ * which is exactly what this method is for an app developer has no
2007
+ * other way to reach them.
1819
2008
  *
1820
2009
  * At most one entry per slug: the current job there, exactly what a
1821
2010
  * per-slug read would answer for that slug. Not a history endpoint.
@@ -1832,18 +2021,22 @@ interface JobsApi {
1832
2021
  * `hello`, having never minted it, and has no other honest value to put
1833
2022
  * there. So this is newest-*known*-first: a job the robot has been
1834
2023
  * running for an hour can sit above one started a minute ago, if the
1835
- * hour-long one was only just adopted (contracts `robotJobsResponse` doc
1836
- * comment).
2024
+ * hour-long one was only just adopted.
1837
2025
  */
1838
2026
  list(robotId: string): Promise<Job[]>;
1839
2027
  }
1840
2028
 
2029
+ /**
2030
+ * One-way messages to a robot's publishers, reachable as
2031
+ * `client.publishers` — a velocity command, a goal pose, anything the
2032
+ * developer exposed as a publisher.
2033
+ */
1841
2034
  interface PublishersApi {
1842
2035
  /**
1843
- * Publishes one message to a publisher (spec §4.2, §6.4).
2036
+ * Publishes one message to a publisher.
1844
2037
  *
1845
2038
  * This is a plain method call — there is deliberately no deadman switch,
1846
- * rate governor or "takt" helper here (spec §14.3). The bridge's own
2039
+ * rate governor or "takt" helper here. The bridge's own
1847
2040
  * `timeout_ms` failsafe is the platform's safety primitive: if messages
1848
2041
  * stop arriving — including because this process crashed — the bridge
1849
2042
  * publishes the configured failsafe message itself. That does **not**
@@ -1854,45 +2047,54 @@ interface PublishersApi {
1854
2047
  * building a publisher-driven control loop.
1855
2048
  *
1856
2049
  * Rejects `publisher_busy` while a different user is publishing and has
1857
- * not been quiet for `quiet_timeout_ms` yet (§6.4) — whoever publishes
2050
+ * not been quiet for its configured quiet timeout yet — whoever publishes
1858
2051
  * holds the publisher implicitly exclusive.
1859
2052
  */
1860
2053
  publish(robotId: string, slug: string, message: Record<string, unknown>, options?: SendCommandOptions): Promise<void>;
1861
2054
  }
1862
2055
 
2056
+ /**
2057
+ * Request/response calls to a robot, reachable as `client.services`. A
2058
+ * service answers once and is done, which is why this namespace has a
2059
+ * single method and nothing to subscribe to.
2060
+ */
1863
2061
  interface ServicesApi {
1864
2062
  /**
1865
- * Calls a service and resolves with its result (spec §4.2, §11.3). A
1866
- * service call is a job underneath — the same `job_id` exchange and
1867
- * disconnect survival as an action (§6.1: "an action goal or a service
1868
- * call") — but that is deliberately invisible here: the caller gets a
2063
+ * Calls a service and resolves with its result. A service call is a job
2064
+ * underneath — the same `job_id` exchange and disconnect survival as an
2065
+ * action but that is deliberately invisible here: the caller gets a
1869
2066
  * plain `Promise<result>`, matching the REST `serviceCallResponse` shape's
1870
2067
  * developer experience. There is nothing to subscribe to for a service —
1871
2068
  * no feedback, no progress, no cancel — so this call already waits for
1872
2069
  * the terminal state internally.
1873
2070
  *
1874
- * `options.patienceMs` bounds the **whole wait** for a service call (W6b)
1875
- * unlike an action, where it bounds acceptance only — because a service
2071
+ * `options.patienceMs` bounds the **whole wait** for a service call
2072
+ * unlike an action, where it bounds acceptance only — because a service
1876
2073
  * has no further state to observe once it settles; the platform gives up
1877
2074
  * on the ROS call itself after this long.
1878
2075
  *
1879
2076
  * `options.timeoutMs` bounds this SDK's own local wait for the WHOLE
1880
2077
  * call — the ack that a job was created, plus however much of the
1881
- * budget is left for it to then reach a terminal state (D10) — not two
2078
+ * budget is left for it to then reach a terminal state — not two
1882
2079
  * separate `timeoutMs`-length windows back to back. A caller who sets
1883
2080
  * `timeoutMs: 5000` is bounding total latency at ~5s, not ~10s; the
1884
2081
  * number means what it says, once, for the whole call.
1885
2082
  *
1886
- * It is also **not independent** of `patienceMs` (D3a): left unset, it
2083
+ * It is also **not independent** of `patienceMs`: left unset, it
1887
2084
  * is derived from `patienceMs` so this SDK's local clock cannot fire
1888
2085
  * before the platform's own deadline has even been reached. Setting
1889
- * both, with `timeoutMs` shorter than `patienceMs`, throws
1890
- * `invalid_option` synchronously rather than letting the two race — see
1891
- * `resolveLocalWaitMs` in `commands.ts` for the full reasoning.
2086
+ * both, with `timeoutMs` shorter than `patienceMs`, rejects with
2087
+ * `invalid_option` before any request is sent rather than letting the two
2088
+ * race see `InvokeOptions.patienceMs` for the full reasoning.
1892
2089
  */
1893
2090
  call(robotId: string, slug: string, params: Record<string, unknown>, options?: InvokeOptions): Promise<unknown>;
1894
2091
  }
1895
2092
 
2093
+ /**
2094
+ * Everything `createClient` accepts. `apiUrl` and `appIdentifier` are
2095
+ * required; the rest either select the kind of caller (`tokenStore` versus
2096
+ * `serverKey`) or replace a global the SDK would otherwise reach for.
2097
+ */
1896
2098
  interface FleetlessClientOptions {
1897
2099
  /** Base URL of the Fleetless REST API, e.g. `https://api.fleetless.dev`. */
1898
2100
  apiUrl: string;
@@ -1918,27 +2120,48 @@ interface FleetlessClientOptions {
1918
2120
  /** Defaults to `apiUrl` with http(s) swapped for ws(s) and `/realtime` appended. */
1919
2121
  realtimeUrl?: string;
1920
2122
  }
2123
+ /**
2124
+ * The settled configuration of a client, reachable as `client.config`. It
2125
+ * is frozen and reflects the defaults `createClient` filled in, which is
2126
+ * what makes it worth reading: `realtimeUrl` is usually derived rather than
2127
+ * passed.
2128
+ */
1921
2129
  interface FleetlessClientConfig {
2130
+ /** The REST base URL this client calls, exactly as passed to `createClient`. */
1922
2131
  readonly apiUrl: string;
2132
+ /** The app this client acts as, exactly as passed to `createClient`. */
1923
2133
  readonly appIdentifier: string;
2134
+ /** The realtime WebSocket URL in use, derived from `apiUrl` unless one was passed. */
1924
2135
  readonly realtimeUrl: string;
1925
2136
  }
2137
+ /**
2138
+ * One app's client, returned by `createClient`. Every API the SDK offers is
2139
+ * a property on it, and all of them share this client's identity, its
2140
+ * single realtime channel and its token refresh.
2141
+ */
1926
2142
  interface FleetlessClient {
2143
+ /** The settled configuration, including the defaults `createClient` filled in. */
1927
2144
  readonly config: FleetlessClientConfig;
2145
+ /** Logging in, logging out, and reading who the caller currently is. */
1928
2146
  readonly auth: AuthApi;
1929
- /** An end user's own consent grants (spec §3.4/§17, W9c) — not available on a `serverKey` client, same reasoning as `auth`'s session-only methods. */
2147
+ /** An end user's own consent grants — not available on a `serverKey` client, same reasoning as `auth`'s session-only methods. */
1930
2148
  readonly grants: GrantsApi;
2149
+ /** A topic's latest value, a live subscription to it, and its recorded history. */
1931
2150
  readonly datapoints: DatapointsApi;
2151
+ /** Long-running work on the robot: invoke, cancel, and watch a job as it runs. */
1932
2152
  readonly actions: ActionsApi;
2153
+ /** Request/response calls to the robot that answer once and are done. */
1933
2154
  readonly services: ServicesApi;
2155
+ /** One-way messages to a robot's publisher, such as a velocity command. */
1934
2156
  readonly publishers: PublishersApi;
2157
+ /** Camera snapshots, their age, and live video sessions. */
1935
2158
  readonly cameras: CamerasApi;
1936
2159
  /**
1937
2160
  * Robot-wide job reads that do not fit under `actions`/`services` because
1938
2161
  * they are not addressed by slug — see `JobsApi.list`.
1939
2162
  */
1940
2163
  readonly jobs: JobsApi;
1941
- /** URDF + mesh reads (spec §4.6) — list/get/urdf, plus the `urdf-loader` mesh callback. */
2164
+ /** URDF and mesh reads — list/get/urdf, plus the `urdf-loader` mesh callback. */
1942
2165
  readonly assets: AssetsApi;
1943
2166
  /**
1944
2167
  * Closes the realtime channel and stops it from reconnecting. Safe to
@@ -1951,6 +2174,16 @@ interface FleetlessClient {
1951
2174
  */
1952
2175
  close(): void;
1953
2176
  }
2177
+ /**
2178
+ * Builds a client for one app. Pass `tokenStore` (or nothing — the default
2179
+ * keeps the session in memory) for an end-user client that logs in with
2180
+ * `auth.login` or the hosted login; pass `serverKey` for a server-side
2181
+ * caller that never holds a user session. Passing both throws, because the
2182
+ * two are different identities and a client acts as exactly one.
2183
+ *
2184
+ * Nothing is fetched here: the realtime channel opens on the first
2185
+ * subscription and closes on `close()` or `auth.logout()`.
2186
+ */
1954
2187
  declare function createClient(options: FleetlessClientOptions): FleetlessClient;
1955
2188
 
1956
- export { type ActionsApi, type Asset, type AssetBytes, type AssetListResponse, type AssetsApi, type AuthApi, type BeginHostedLoginOptions, type BusyDetails, type CameraDescriptor, type CameraLiveSession, type CameraSnapshot, type CameraSnapshotMeta, type CamerasApi, type ClientIdentity, type CompleteHostedLoginOptions, type ConsentGrantSummary, type ConsentRevokeResponse, type CreateMeshLoaderOptions, type DatapointEvent, type DatapointSubscription, type DatapointSubscriptionHandlers, type DatapointValue, type DatapointsApi, type FleetlessClient, type FleetlessClientConfig, type FleetlessClientOptions, FleetlessError, type FleetlessErrorCode, type FleetlessErrorOptions, type GrantsApi, type HistoryAggregation, type HistoryBucketsResponse, type HistoryOptions, type HistorySamplesResponse, type HostedLoginRequest, InMemoryTokenStore, type Job, type JobEvent, type JobState, type JobSubscription, type JobSubscriptionHandlers, type JobsApi, type LogoutResult, type MeshLoaderDelegate, type ParameterInvalidDetails, type ParameterViolation, type PrepareUrdfSceneOptions, type PublishersApi, type RateLimitDetails, SDK_ERROR_CODES, type SdkErrorCode, type SendCommandOptions, type ServicesApi, type StoredSession, type TokenStore, type UrdfCompleteness, type UrdfSceneManager, type UrdfSceneResources, createClient, parameterInvalidDetails };
2189
+ export { type ActionsApi, type Asset, type AssetBytes, type AssetListResponse, type AssetsApi, type AuthApi, type BeginHostedLoginOptions, type BusyDetails, type CameraDescriptor, type CameraLiveSession, type CameraSnapshot, type CameraSnapshotMeta, type CamerasApi, type ClientIdentity, type CompleteHostedLoginOptions, type ConsentGrantSummary, type ConsentRevokeResponse, type CreateMeshLoaderOptions, type DatapointEvent, type DatapointSubscription, type DatapointSubscriptionHandlers, type DatapointValue, type DatapointsApi, type FleetlessClient, type FleetlessClientConfig, type FleetlessClientOptions, FleetlessError, type FleetlessErrorCode, type FleetlessErrorOptions, type GrantsApi, type HistoryAggregation, type HistoryBucketsResponse, type HistoryOptions, type HistorySamplesResponse, type HostedLoginRequest, InMemoryTokenStore, type InvokeOptions, type Job, type JobEvent, type JobState, type JobSubscription, type JobSubscriptionHandlers, type JobsApi, type LogoutResult, type MeshLoaderDelegate, type ParameterInvalidDetails, type ParameterViolation, type PrepareUrdfSceneOptions, type PublishersApi, type RateLimitDetails, SDK_ERROR_CODES, type SdkErrorCode, type SendCommandOptions, type ServicesApi, type StoredSession, type TokenStore, type UrdfCompleteness, type UrdfSceneManager, type UrdfSceneResources, createClient, parameterInvalidDetails };