@fleetless/sdk 3.0.2 → 3.0.3

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/README.md CHANGED
@@ -5,82 +5,49 @@
5
5
  [![license MIT](https://img.shields.io/npm/l/@fleetless/sdk)](LICENSE)
6
6
  [![node >=20](https://img.shields.io/node/v/@fleetless/sdk)](https://nodejs.org)
7
7
 
8
- The official TypeScript SDK for apps built on [Fleetless](https://fleetless.dev),
9
- which exposes a ROS 2 robot as a hosted REST and realtime API — exactly the
10
- topics, services and actions a developer chose to publish, under stable slugs,
11
- behind roles. The SDK covers all of it: datapoints (read, subscribe, recorded
12
- history), actions, services, publishers, cameras, jobs, assets and URDF, and
13
- the whole client auth API your own sign-in UI calls. It is framework-agnostic,
14
- ships ESM and CJS with its own types, and runs wherever a `fetch` and a
15
- `WebSocket` exist — a browser, a mobile webview, Node 22 or newer, and
16
- server-side with a server key instead of a user session. On Node 20 the REST
17
- half works as it stands, but there is no global `WebSocket` (it is behind
18
- `--experimental-websocket` there), so anything realtime needs one supplied:
19
- `createClient({ …, WebSocket: (await import('ws')).WebSocket })`, or the flag.
20
- Without it, `subscribe` calls `onError` with `no_websocket`.
8
+ **Your robot, as an API. This is the client.**
21
9
 
22
- **The mental model in one paragraph.** A robot runs the Fleetless bridge, a
23
- ROS 2 node. In the [Fleetless Console](https://console.fleetless.dev) a
24
- developer picks which topics, services and actions that robot exposes and gives
25
- each one a stable `snake_case` slug and a role that may reach it. The cloud
26
- turns those into REST and realtime endpoints. This package is the client for
27
- those endpoints: you address a robot by its id and a capability by its slug,
28
- you sign your own users in against your own app's user directory, and you never
29
- see a ROS name, a topic type or a message definition. Renaming a node on the
30
- robot is invisible to your app; changing a slug is not.
10
+ [Fleetless](https://fleetless.dev) turns a ROS 2 robot into a hosted REST and
11
+ realtime API. The robot runs the Fleetless Bridge, and in the
12
+ [Fleetless Console](https://console.fleetless.dev) a developer picks which
13
+ topics, services, actions, publishers and cameras it exposes, each under a
14
+ stable slug and behind a role. Your app talks to those slugs over HTTPS and
15
+ never learns a ROS name, a topic type or a message definition.
31
16
 
32
- New here? The [getting-started guide](https://docs.fleetless.dev/getting-started/)
33
- goes from a robot that has never connected to a value on a page, and the
34
- [app-auth recipe](https://docs.fleetless.dev/recipes/app-auth/) is the
35
- walkthrough for every sign-in screen.
17
+ This package is the official TypeScript client for that API. It handles the
18
+ sessions, the reconnects, the subscriptions and the errors, so your code can
19
+ get on with the part that is actually about your robot. Framework-agnostic,
20
+ ESM and CJS, types included.
36
21
 
37
- ## Why
38
-
39
- - **One connection, however many subscriptions.** Every datapoint and job
40
- subscription shares a single authenticated WebSocket. It reconnects with
41
- exponential backoff, re-authenticates, and resends its own subscribe frames,
42
- so a dropped network shows up as a gap in events rather than as work for you.
43
- - **Subscriptions are reference-counted per robot and slug.** Two widgets on
44
- the same battery value, or one component mounted twice under React
45
- StrictMode, share one wire subscription. Unsubscribing one never cuts off the
46
- other.
47
- - **Errors you branch on, not errors you parse.** Every refusal arrives as a
48
- `FleetlessError` carrying a stable `code` — `busy`, `forbidden`,
49
- `robot_offline`, `rate_limited` — plus the SDK's own codes for refusals that
50
- never reached the network. `rate_limited` is surfaced with its
51
- `retry_after_ms` and never retried behind your back.
52
- - **Publishers with a failsafe the robot enforces.** `publishers.publish` sends
53
- one message and nothing else. If your app stops publishing — including
54
- because it crashed — the bridge on the robot publishes the configured
55
- failsafe message itself. The safety primitive lives on the robot, not in your
56
- event loop.
57
- - **Cameras that cost nothing while nobody watches.** Snapshot bytes come with
58
- the cloud's own `age_ms`, and keep being served while the robot is offline.
59
- `cameras.live` takes a refcounted hold: the first viewer starts the robot
60
- publishing, the last one leaving stops it. You get a LiveKit room URL and
61
- token to hand to the LiveKit client of your choice.
62
- - **The whole auth surface, and every screen stays yours.** Fleetless renders
63
- no page for an app user: registration with email verification, invitations,
64
- password reset, single sign-on to one of the app's identity providers, and
65
- the MCP consent screen are all methods on `client.auth`, called from a UI you
66
- wrote. The federated flow runs its own PKCE (S256 only) against Fleetless;
67
- `beginOidcLogin` makes no network call, and `completeOidcLogin` checks
68
- `state` before it exchanges anything.
69
- - **A token store you control.** Implement two methods to keep a session in
70
- `localStorage`, a cookie or a native keystore; the default keeps it in
71
- memory. Refresh is silent and single-flight, so ten concurrent calls that
72
- meet an expired access token share one refresh instead of firing ten.
22
+ Fleetless is in closed beta. The waiting list is at
23
+ <https://fleetless.dev/#waiting-list>.
73
24
 
74
- ## Getting started
25
+ ## ✨ What you can do with it
26
+
27
+ - **Read datapoints** once, subscribe to them live over one reconnecting
28
+ WebSocket, or query their recorded history.
29
+ - **Run actions and call services** by slug, with feedback, progress and
30
+ results as they arrive.
31
+ - **Publish messages** to a topic, behind a failsafe the robot enforces
32
+ itself the moment your app goes quiet.
33
+ - **Show cameras**: a snapshot that is always there, or live video over
34
+ WebRTC that starts with the first viewer and stops with the last.
35
+ - **Follow jobs**, sync assets, and render the robot's URDF with its meshes.
36
+ - **Sign your users in** — registration, verification, invitations, password
37
+ reset, single sign-on and MCP consent — from screens that are entirely
38
+ yours. Fleetless renders no page for an app user.
39
+ - **Branch on errors** instead of parsing them: every refusal is a
40
+ `FleetlessError` with a stable `code`.
41
+
42
+ ## 🚀 Getting started
75
43
 
76
44
  ```sh
77
45
  npm i @fleetless/sdk
78
46
  ```
79
47
 
80
- You need an app identifier and the credentials of an **app user** — an
81
- account inside that app, which the developer's team creates or invites from
82
- the [Fleetless Console](https://console.fleetless.dev). A console login is a
83
- Fleetless user, a different identity space, and will not sign in here.
48
+ You need an app identifier and an **app user** of that app, both created in
49
+ the console. A console login is a Fleetless user, a different identity space,
50
+ and will not sign in here.
84
51
 
85
52
  ```ts checked
86
53
  import { createClient } from '@fleetless/sdk'
@@ -92,312 +59,65 @@ const client = createClient({
92
59
 
93
60
  await client.auth.login('user@example.com', 'correct-horse-battery')
94
61
 
95
- // Robot ids are UUIDs; the console shows the one you want.
96
- const robotId = '4f2c1a90-7b3e-4d51-9c86-0a1b2c3d4e5f'
62
+ const robotId = '4f2c1a90-7b3e-4d51-9c86-0a1b2c3d4e5f' // the console shows it
97
63
 
98
64
  // One-shot read.
99
65
  const battery = await client.datapoints.get(robotId, 'battery_percentage')
100
66
  console.log(battery.value, battery.timestamp_ms)
101
67
 
102
- // Live updates. The current value arrives immediately, then every change.
68
+ // Live updates: the current value first, then every change.
103
69
  const subscription = client.datapoints.subscribe(robotId, 'battery_percentage', {
104
- onEvent(event) {
105
- console.log(event.value, event.timestamp_ms)
106
- },
107
- onError(error) {
108
- console.error(error.code, error.message)
109
- },
70
+ onEvent: (event) => console.log(event.value, event.timestamp_ms),
71
+ onError: (error) => console.error(error.code, error.message),
110
72
  })
111
73
 
112
74
  // Later:
113
75
  subscription.unsubscribe()
114
76
  await client.auth.logout()
115
- // logout() already closes the realtime channel. Call close() yourself when a
116
- // process should exit without logging out — an open WebSocket keeps Node alive.
117
- client.close()
118
- ```
119
-
120
- Slugs are the names you chose in the console, in `snake_case`
121
- (`[a-z][a-z0-9]*(_[a-z0-9]+)*`) — never the robot's internal ROS names, which
122
- is what makes renaming a node on the robot invisible to your app.
123
-
124
- ### Your own sign-in UI
125
-
126
- Every screen an app user sees belongs to your app. These are the calls behind
127
- them; the full walkthrough, page by page, is the
128
- [app-auth recipe](https://docs.fleetless.dev/recipes/app-auth/).
129
-
130
- ```ts
131
- // Registration. Resolves on 202 — which says a policy-allowed request was
132
- // accepted, NOT that an account was created. An address the app already knows
133
- // gets the identical answer with no mail sent, so the only honest thing to
134
- // render is a sentence about the mailbox.
135
- await client.auth.register({ email, password, displayName })
136
-
137
- // The page your app serves at the app's configured `verify_url`. Stores a
138
- // session, so nobody types a password right after reading their mail.
139
- await client.auth.verifyEmail(tokenFromTheLink)
140
-
141
- // Recovery. requestPasswordReset always resolves on 202, for a known address
142
- // and an unknown one alike.
143
- await client.auth.requestPasswordReset(email)
144
- await client.auth.confirmPasswordReset(tokenFromTheLink, newPassword)
145
-
146
- // The page your app serves at the app's configured `invite_url`.
147
- await client.auth.acceptInvitation({ token: tokenFromTheLink, password })
148
- ```
149
-
150
- ### Sign-in through an identity provider
151
-
152
- An app may carry any number of OpenID Connect providers, configured per app in
153
- the console. Your page draws the buttons; your app never talks to the provider.
154
-
155
- ```ts
156
- // Draw the buttons. Only enabled providers are listed, slug and name only.
157
- const providers = await client.auth.listProviders()
158
-
159
- // 1. Build the URL. No network call: this generates the PKCE verifier and the
160
- // state, and hands them back, because a redirect is a fresh page load.
161
- const request = await client.auth.beginOidcLogin({
162
- slug: 'azure',
163
- redirectUri: 'https://your-app.example.com/callback',
164
- })
165
- sessionStorage.setItem('fleetless_state', request.state)
166
- sessionStorage.setItem('fleetless_code_verifier', request.codeVerifier)
167
- window.location.assign(request.url)
168
-
169
- // 2. On the callback page. A refused sign-in redirects here with `error`
170
- // rather than a code, so check for that first.
171
- const params = new URL(window.location.href).searchParams
172
- const failure = client.auth.oidcErrorFromCallback(params)
173
- if (failure) {
174
- // A FleetlessError carrying the documented reason: no_access, email_taken,
175
- // email_unverified, domain_not_allowed, registration_closed, and the rest.
176
- showSignInProblem(failure)
177
- } else {
178
- await client.auth.completeOidcLogin({
179
- code: params.get('code') ?? '',
180
- state: params.get('state') ?? '',
181
- expectedState: sessionStorage.getItem('fleetless_state') ?? '',
182
- codeVerifier: sessionStorage.getItem('fleetless_code_verifier') ?? '',
183
- })
184
- // From here on me(), logout() and silent refresh behave exactly as they do
185
- // after login().
186
- }
187
77
  ```
188
78
 
189
- `completeOidcLogin` refuses before any network call, with `state_mismatch`,
190
- for a mismatch, a missing `state`, **and** an empty `expectedState` — that
191
- last case is checked first and explicitly, because two empty strings compare
192
- equal.
193
-
194
- One callback URL is registered at the provider,
195
- `https://api.fleetless.dev/api/client/oidc/callback`, the same for every app
196
- and every provider. It is not your `redirectUri`, which is a page in your app.
79
+ `battery_percentage` is a slug you chose in the console, not a ROS topic.
80
+ Rename the node on the robot and your app never notices. Rename the slug and
81
+ it does.
197
82
 
198
- ### The MCP consent screen
83
+ The SDK runs wherever `fetch` and `WebSocket` exist: browsers, webviews,
84
+ Node 22 and newer, or server-side with a server key instead of a user session.
85
+ Node 20 does REST fine but ships no global `WebSocket`, so realtime there needs
86
+ one passed in through the `WebSocket` option.
199
87
 
200
- Only if the app serves its own MCP endpoint. An authorization redirects the
201
- browser to a page your app serves, with an interaction id; your page signs the
202
- person in, shows them what is being asked, and answers.
203
-
204
- ```ts
205
- // Sign the person in FIRST — `already_granted` is derived from the bearer,
206
- // and an anonymous read answers `false` for everybody.
207
- const interaction = await client.auth.mcpInteraction(interactionId)
208
-
209
- // `interaction.client_name` is a name the client typed about itself during an
210
- // unauthenticated registration. `client_name_verified` is the literal false;
211
- // render it as a claim, never as an identity. `scopes` is always empty: what a
212
- // session reaches is decided by the user's role, re-read on every call.
213
- const { redirectTo } = approved
214
- ? await client.auth.approveMcpInteraction(interactionId)
215
- : await client.auth.denyMcpInteraction(interactionId)
216
- window.location.assign(redirectTo) // a denial redirects too
217
-
218
- // A "connected apps" screen. Withdrawing stops the NEXT authorization; a
219
- // session already running keeps working for the rest of its 15 minutes.
220
- const grants = await client.auth.listMcpGrants()
221
- await client.auth.revokeMcpGrant(grants[0].client_id)
222
- ```
223
-
224
- ### Actions
225
-
226
- An action is a job with a lifecycle. `invoke` resolves as soon as the job
227
- exists; feedback, progress and the result arrive over `subscribe`.
228
-
229
- ```ts
230
- const job = await client.actions.invoke(robotId, 'dock', { 'target_pose.position.x': 1.0 })
231
-
232
- const sub = client.actions.subscribe(robotId, 'dock', {
233
- onJob(event) {
234
- console.log(event.job.state, event.feedback, event.progress)
235
- if (event.job.state === 'succeeded') console.log(event.job.result)
236
- },
237
- onError(error) {
238
- console.error(error.code, error.message)
239
- },
240
- })
241
-
242
- // Cancel the job you started, specifically — not whatever runs there by now.
243
- const cancelled = await client.actions.cancel(robotId, 'dock', job.id)
244
- sub.unsubscribe()
245
- ```
88
+ ## 📚 Documentation
246
89
 
247
- `params` is flat, keyed by the parameter names the developer declared. A second
248
- `invoke` while one is still running is refused `busy`, with the running job on
249
- `error.details.running`. A service is the same machinery with a plain result:
250
- `await client.services.call(robotId, 'set_mode', { mode: 'autonomous' })`.
90
+ Everything past this point lives at **[docs.fleetless.dev](https://docs.fleetless.dev)**.
251
91
 
252
- ### Publishers, and no teleop helpers
253
-
254
- `publishers.publish` sends **one** message. There is no `startPublishing`, no
255
- rate helper, no joystick binding and no "hold this value" call, and that is a
256
- decision rather than a gap.
257
-
258
- The safety primitive lives on the robot. A publisher is configured with a
259
- `timeout_ms` and a failsafe message; if messages stop arriving — including
260
- because your process crashed, your tab closed, or the network went away — the
261
- bridge publishes the failsafe itself. Nothing in your event loop has to survive
262
- for that to happen.
263
-
264
- A helper here would move the appearance of that guarantee into a browser, where
265
- it is not true. So *how often* and *when* to publish is your application's
266
- decision, made where it can see the user's intent:
267
-
268
- ```ts
269
- // A control loop is yours to write, and yours to stop. `speed` is your app's.
270
- const timer = setInterval(() => {
271
- void client.publishers.publish(robotId, 'cmd_vel', { 'linear.x': speed })
272
- }, 100)
273
-
274
- // Stop publishing and let the robot's own failsafe take over.
275
- clearInterval(timer)
276
- ```
277
-
278
- `publish` rejects `publisher_busy` while a different user holds the publisher
279
- and has not been quiet for its configured timeout.
280
-
281
- ### Cameras
282
-
283
- ```ts
284
- import { Room } from 'livekit-client' // your choice of LiveKit client
285
-
286
- // The snapshot is always there, independent of anyone watching live.
287
- const snap = await client.cameras.snapshot(robotId, 'camera_front')
288
- if (snap.image !== null) console.log(snap.mime, `${snap.age_ms}ms old`)
289
-
290
- // Live is on demand, and it costs you a release.
291
- const room = new Room()
292
- const session = await client.cameras.live(robotId, 'camera_front')
293
- await room.connect(session.url, session.token)
294
-
295
- // When you are done watching — both calls, together. Disconnecting the Room is
296
- // what actually stops the stream; release() is the courteous fast path.
297
- await room.disconnect()
298
- await session.release()
299
- ```
300
-
301
- ## Errors
302
-
303
- Every refusal arrives as a `FleetlessError` with a stable `code`. Branch on the
304
- code; never parse the message, which is written for a developer reading a
305
- console and may change.
306
-
307
- `error.details` is typed `unknown`, deliberately: it is whatever the server
308
- sent, and the SDK does not pretend to have validated it. Narrow it before you
309
- read a field. `BusyDetails` and `RateLimitDetails` are exported as types, and
310
- `parameterInvalidDetails` as a runtime schema you can `parse`.
311
-
312
- ```ts checked
313
- import { createClient, FleetlessError } from '@fleetless/sdk'
314
- import type { BusyDetails, RateLimitDetails } from '@fleetless/sdk'
315
-
316
- declare function showRunning(job: BusyDetails['running'] | undefined): void
317
- declare function showOffline(): void
318
- declare function showNotAllowed(): void
319
- declare function showRetryLater(ms: number | undefined): void
320
- declare function showUnexpected(error: FleetlessError): void
321
-
322
- const client = createClient({ apiUrl: 'https://api.fleetless.dev', appIdentifier: 'warehouse_dash' })
323
- const robotId = '4f2c1a90-7b3e-4d51-9c86-0a1b2c3d4e5f'
324
-
325
- /** `details` is server-shaped: check the field is there before reading it. */
326
- const field = <T,>(details: unknown, key: string): T | undefined =>
327
- typeof details === 'object' && details !== null && key in details
328
- ? ((details as Record<string, unknown>)[key] as T)
329
- : undefined
330
-
331
- try {
332
- await client.actions.invoke(robotId, 'dock', {})
333
- } catch (error) {
334
- if (!(error instanceof FleetlessError)) throw error
335
- switch (error.code) {
336
- case 'busy': showRunning(field<BusyDetails['running']>(error.details, 'running')); break
337
- case 'robot_offline': showOffline(); break
338
- case 'forbidden': showNotAllowed(); break
339
- case 'rate_limited': showRetryLater(field<RateLimitDetails['retry_after_ms']>(error.details, 'retry_after_ms')); break
340
- default: showUnexpected(error); break
341
- }
342
- }
343
- ```
344
-
345
- - **`rate_limited` is surfaced, never retried behind your back.** The SDK does
346
- not sleep and re-send. `details.retry_after_ms` carries the wait the platform
347
- asked for, when it sent one — read it defensively, since a refusal from an
348
- intermediary may carry no `details` at all.
349
- - **The one retry the SDK does perform is `token_expired`**, and only once: it
350
- refreshes the session and re-sends the same request. That refresh is
351
- single-flight, so ten concurrent calls meeting an expired token share one.
352
- - **`command_outcome_unknown` is an honest answer, not a failure.** It means
353
- the realtime connection cycled while a command was in flight and the SDK
354
- cannot say whether the robot got it. Read the job back by slug through
355
- `actions.subscribe` to find out.
356
- - Some codes never reach the network at all — `invalid_option`,
357
- `state_mismatch`, `no_session`, `untrusted_absolute_url`. They are the SDK
358
- refusing to send something rather than the platform refusing to accept it.
359
-
360
- ## Documentation and help
361
-
362
- - **[SDK reference](https://docs.fleetless.dev/reference/sdk/)** — every method,
363
- every option, and what each one deliberately does not do.
92
+ - **[SDK reference](https://docs.fleetless.dev/reference/sdk/)** — every
93
+ method, every option, and what each one deliberately does not do.
364
94
  - **[Getting started](https://docs.fleetless.dev/getting-started/)** — from a
365
95
  robot that has never connected to a value in your app.
366
- - **[REST and realtime API](https://docs.fleetless.dev/reference/api/)** — the
367
- wire surface underneath this package.
368
96
  - **[Your own login UI](https://docs.fleetless.dev/recipes/app-auth/)** — the
369
- full walkthrough for every auth screen, and the mistakes that cost the most
370
- time.
97
+ walkthrough for every sign-in screen.
371
98
  - **[Identity](https://docs.fleetless.dev/reference/identity/)** — the two
372
- identity spaces, the federation rules, and what each refusal licenses your
373
- UI to claim.
99
+ identity spaces, and what a refusal licenses your UI to claim.
100
+ - **[REST and realtime API](https://docs.fleetless.dev/reference/api/)** —
101
+ the wire underneath this package.
374
102
  - **[CHANGELOG.md](CHANGELOG.md)** — what changed in each version.
375
- - Questions, bug reports and feature requests: hello@fleetless.dev.
376
103
 
377
- Fleetless is in closed beta. The waiting list is at
378
- <https://fleetless.dev/#waiting-list>.
104
+ Questions, bug reports and feature requests: hello@fleetless.dev.
379
105
 
380
- ## Reporting a security issue
106
+ ## 🔒 Reporting a security issue
381
107
 
382
- Email **security@fleetless.dev**. Please do not open a public issue for a
383
- security report. [SECURITY.md](SECURITY.md) says what is in scope here — how
384
- this package handles tokens, the PKCE verifier and the `state` value — and what
385
- belongs to the platform instead.
108
+ Email **security@fleetless.dev** rather than opening a public issue.
109
+ [SECURITY.md](SECURITY.md) says what is in scope for this package — tokens,
110
+ the PKCE verifier, the `state` value — and what belongs to the platform.
386
111
 
387
- ## Contributing
112
+ ## 🤝 Contributing
388
113
 
389
114
  The public repository is not open yet. Until it is, send patches and questions
390
- to <hello@fleetless.dev>. Read
391
- [CONTRIBUTING.md](CONTRIBUTING.md) first: it covers the setup, the checks, the
392
- Contributor Licence Agreement, and the one rule this repository is strict about
393
- — the auth suites drive the SDK's own `fetch` against a real `node:http`
394
- server rather than a double, because a mocked `fetch` cannot see the bugs that
395
- have actually shipped.
115
+ to <hello@fleetless.dev>, after reading [CONTRIBUTING.md](CONTRIBUTING.md) for
116
+ the setup, the checks and the Contributor Licence Agreement.
396
117
 
397
118
  By participating you agree to the [Code of Conduct](CODE_OF_CONDUCT.md).
398
119
 
399
- ## Maintainers
400
-
401
- Maintained by [Dehne Robotik GmbH](https://dehne-robotik.de).
120
+ ## 📜 Licence
402
121
 
403
- MIT licensed — see [LICENSE](LICENSE).
122
+ MIT — see [LICENSE](LICENSE). Maintained by
123
+ [Dehne Robotik GmbH](https://dehne-robotik.de).
package/SECURITY.md CHANGED
@@ -18,7 +18,7 @@ released fix, we credit you by name unless you ask us not to.
18
18
 
19
19
  This repository is the TypeScript SDK that client applications use to talk to
20
20
  Fleetless. It runs **in your users' browsers and on your servers**, and it
21
- holds credentials while it does. That is where its security surface is.
21
+ holds credentials while it does. That is its security surface.
22
22
 
23
23
  Specifically in scope:
24
24
 
@@ -39,14 +39,14 @@ them in memory only and writes them nowhere. A report is in scope if the SDK:
39
39
  - races its own silent refresh in a way that lets a stale or a foreign token
40
40
  be used.
41
41
 
42
- Choosing to persist tokens in `localStorage`, a cookie or a native keystore is
43
- your decision and its consequences are yours; a defect in how the SDK *hands*
44
- them to your store is ours.
42
+ Where you persist tokens — `localStorage`, a cookie, a native keystore — is
43
+ your decision and your risk. A defect in how the SDK *hands* them to your
44
+ store is ours.
45
45
 
46
46
  ### PKCE and `state` in the federated sign-in flow
47
47
 
48
- `beginOidcLogin` generates a PKCE verifier and a `state` value and returns them
49
- to you, because a redirect is a fresh page load and the SDK has nowhere of its
48
+ `beginOidcLogin` generates a PKCE verifier and a `state` value and hands them
49
+ back to you: a redirect is a fresh page load, and the SDK has nowhere of its
50
50
  own to keep them. `completeOidcLogin` checks `state` before it exchanges
51
51
  anything.
52
52
 
@@ -58,7 +58,7 @@ equal; any path that reaches the token exchange without the check.
58
58
 
59
59
  **Where you store the verifier and the `state` between the two calls is your
60
60
  application's decision**, and this policy cannot cover it. The README shows
61
- `sessionStorage`, which is a reasonable default and not the only correct one.
61
+ `sessionStorage`, a reasonable default and not the only correct one.
62
62
 
63
63
  ### The rest of the package
64
64
 
@@ -79,10 +79,10 @@ application's decision**, and this policy cannot cover it. The README shows
79
79
  **The Fleetless cloud is not in this repository.** A server that fails to
80
80
  enforce a permission, an authentication or authorisation flaw in the platform,
81
81
  a rate limit, a data leak from an API endpoint, anything about how a token is
82
- minted or validated — none of those live here, and none of them can be fixed by
83
- a change to this package. Report them to the same address; say which service
84
- you were looking at, and we will route it. What we cannot do is treat this
85
- repository's issue tracker as the place where they are tracked.
82
+ minted or validated — none of that lives here, and none of it can be fixed by
83
+ a change to this package. Report it to the same address, say which service you
84
+ were looking at, and we will route it. We will not treat this repository's
85
+ issue tracker as the place where it is tracked.
86
86
 
87
87
  Also out of scope here: the Fleetless console, the robot-side bridge, the
88
88
  `@fleetless/contracts` schemas (they have their own repository and their own
package/dist/index.cjs CHANGED
@@ -334,7 +334,7 @@ function createAssetsApi(http) {
334
334
  finish(
335
335
  null,
336
336
  new Error(
337
- `createMeshLoader and prepareUrdfScene were both installed on the same LoadingManager for robot ${robotId}. Use one or the other on a given manager, not both \u2014 see the README.`
337
+ `createMeshLoader and prepareUrdfScene were both installed on the same LoadingManager for robot ${robotId}. Use one or the other on a given manager, not both \u2014 see the SDK reference.`
338
338
  )
339
339
  );
340
340
  return;
@@ -8332,21 +8332,21 @@ function assertValidJobId(jobId) {
8332
8332
  if (jobId === void 0 || jobId === null || typeof jobId === "string") return;
8333
8333
  throw new FleetlessError(
8334
8334
  "invalid_option",
8335
- `cancel()'s third argument must be a job id (string), null, or omitted \u2014 got ${typeof jobId === "object" ? "an object" : typeof jobId}. If you are passing an options object (e.g. {timeoutMs}) as the third argument, note the signature changed in this release: cancel(robotId, slug) is unchanged, but a third positional argument is now the job id to cancel and options moved to a fourth argument \u2014 cancel(robotId, slug, jobId, options). See the README's Actions section.`
8335
+ `cancel()'s third argument must be a job id (string), null, or omitted \u2014 got ${typeof jobId === "object" ? "an object" : typeof jobId}. If you are passing an options object (e.g. {timeoutMs}) as the third argument, note the signature changed in this release: cancel(robotId, slug) is unchanged, but a third positional argument is now the job id to cancel and options moved to a fourth argument \u2014 cancel(robotId, slug, jobId, options). See the Actions section of the SDK reference at https://docs.fleetless.dev/reference/sdk/`
8336
8336
  );
8337
8337
  }
8338
8338
  function createRealtimeCommandTransport(channel) {
8339
8339
  return {
8340
- // Declared `async` deliberately, unlike `cancel`/`publish` below: it is
8340
+ // Declared `async` deliberately, unlike `cancel`/`publish` below: it's
8341
8341
  // the only one of the three that can refuse *before* sending anything
8342
- // (`resolveLocalWaitMs`'s `invalid_option`), and every caller of
8343
- // this interface — starting with this file's own `sendCommand` callers
8344
- // — is entitled to assume `CommandTransport.invoke` always returns a
8345
- // promise rather than throwing synchronously. Without `async` here, a
8346
- // synchronous throw from `resolveLocalWaitMs` would escape as a thrown
8347
- // exception instead of a rejection, breaking that assumption for any
8348
- // caller that isn't itself inside an `async` function (e.g. a test
8349
- // calling this transport directly).
8342
+ // (`resolveLocalWaitMs`'s `invalid_option`), and every caller of this
8343
+ // interface — starting with this file's own `sendCommand` callers —
8344
+ // assumes `CommandTransport.invoke` always returns a promise, never
8345
+ // throws synchronously. Without `async` here, a synchronous throw from
8346
+ // `resolveLocalWaitMs` would escape as a thrown exception instead of a
8347
+ // rejection, breaking that assumption for a caller that isn't itself
8348
+ // inside an `async` function (e.g. a test calling this transport
8349
+ // directly).
8350
8350
  async invoke(robotId, slug2, params, options) {
8351
8351
  const timeoutMs = resolveLocalWaitMs(options ?? {}, DEFAULT_COMMAND_TIMEOUT_MS);
8352
8352
  const frame = {
@@ -8546,12 +8546,11 @@ var RealtimeChannel = class {
8546
8546
  }
8547
8547
  /**
8548
8548
  * Increments on every successful authentication (first connect and every
8549
- * reconnect). A command sent on one physical socket can only ever be
8550
- * answered on that socket — comparing the epoch captured at send time
8551
- * against the current one is how a caller (see `commands.ts`) tells "still
8552
- * waiting on the connection it was sent over" from "that connection is
8553
- * gone and a new one has taken its place", the moment it happens rather
8554
- * than after a timeout elapses.
8549
+ * reconnect). A command sent on one socket can only be answered on that
8550
+ * socket — comparing the epoch at send time against the current one is
8551
+ * how a caller (see `commands.ts`) tells "still waiting on the connection
8552
+ * it was sent over" from "that connection is gone and a new one has taken
8553
+ * its place", the moment it happens rather than after a timeout elapses.
8555
8554
  */
8556
8555
  get connectionEpoch() {
8557
8556
  return this.#connectionEpoch;
@@ -8852,7 +8851,7 @@ function createSlugSubscriptions(channel) {
8852
8851
  // src/token-store.ts
8853
8852
  var InMemoryTokenStore = class {
8854
8853
  #session = null;
8855
- /** Creates an empty store. Nothing is loaded from anywhere — a client built with it starts logged out. */
8854
+ /** Nothing is loaded from anywhere — a client built with it starts logged out. */
8856
8855
  constructor() {
8857
8856
  }
8858
8857
  /** Returns the session held in memory, or `null` if there is none. */