@fleetless/sdk 3.0.2 → 3.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/README.md CHANGED
@@ -5,82 +5,51 @@
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
+ - **Find your robots** and read what your role lets you do on each — slugs,
28
+ units, parameter schemas — before you draw a screen.
29
+ - **Read datapoints** once, subscribe to them live over one reconnecting
30
+ WebSocket, or query their recorded history.
31
+ - **Run actions and call services** by slug, with feedback, progress and
32
+ results as they arrive.
33
+ - **Publish messages** to a topic, behind a failsafe the robot enforces
34
+ itself the moment your app goes quiet.
35
+ - **Show cameras**: a snapshot that is always there, or live video over
36
+ WebRTC that starts with the first viewer and stops with the last.
37
+ - **Follow jobs**, sync assets, and render the robot's URDF with its meshes.
38
+ - **Sign your users in** — registration, verification, invitations, password
39
+ reset, single sign-on and MCP consent — from screens that are entirely
40
+ yours. Fleetless renders no page for an app user.
41
+ - **Branch on errors** instead of parsing them: every refusal is a
42
+ `FleetlessError` with a stable `code`.
43
+
44
+ ## 🚀 Getting started
75
45
 
76
46
  ```sh
77
47
  npm i @fleetless/sdk
78
48
  ```
79
49
 
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.
50
+ You need an app identifier and an **app user** of that app, both created in
51
+ the console. A console login is a Fleetless user, a different identity space,
52
+ and will not sign in here.
84
53
 
85
54
  ```ts checked
86
55
  import { createClient } from '@fleetless/sdk'
@@ -92,312 +61,65 @@ const client = createClient({
92
61
 
93
62
  await client.auth.login('user@example.com', 'correct-horse-battery')
94
63
 
95
- // Robot ids are UUIDs; the console shows the one you want.
96
- const robotId = '4f2c1a90-7b3e-4d51-9c86-0a1b2c3d4e5f'
64
+ const robotId = '4f2c1a90-7b3e-4d51-9c86-0a1b2c3d4e5f' // the console shows it
97
65
 
98
66
  // One-shot read.
99
67
  const battery = await client.datapoints.get(robotId, 'battery_percentage')
100
68
  console.log(battery.value, battery.timestamp_ms)
101
69
 
102
- // Live updates. The current value arrives immediately, then every change.
70
+ // Live updates: the current value first, then every change.
103
71
  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
- },
72
+ onEvent: (event) => console.log(event.value, event.timestamp_ms),
73
+ onError: (error) => console.error(error.code, error.message),
110
74
  })
111
75
 
112
76
  // Later:
113
77
  subscription.unsubscribe()
114
78
  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
79
  ```
188
80
 
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.
81
+ `battery_percentage` is a slug you chose in the console, not a ROS topic.
82
+ Rename the node on the robot and your app never notices. Rename the slug and
83
+ it does.
197
84
 
198
- ### The MCP consent screen
85
+ The SDK runs wherever `fetch` and `WebSocket` exist: browsers, webviews,
86
+ Node 22 and newer, or server-side with a server key instead of a user session.
87
+ Node 20 does REST fine but ships no global `WebSocket`, so realtime there needs
88
+ one passed in through the `WebSocket` option.
199
89
 
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
- ```
90
+ ## 📚 Documentation
246
91
 
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' })`.
92
+ Everything past this point lives at **[docs.fleetless.dev](https://docs.fleetless.dev)**.
251
93
 
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.
94
+ - **[SDK reference](https://docs.fleetless.dev/reference/sdk/)** — every
95
+ method, every option, and what each one deliberately does not do.
364
96
  - **[Getting started](https://docs.fleetless.dev/getting-started/)** — from a
365
97
  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
98
  - **[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.
99
+ walkthrough for every sign-in screen.
371
100
  - **[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.
101
+ identity spaces, and what a refusal licenses your UI to claim.
102
+ - **[REST and realtime API](https://docs.fleetless.dev/reference/api/)** —
103
+ the wire underneath this package.
374
104
  - **[CHANGELOG.md](CHANGELOG.md)** — what changed in each version.
375
- - Questions, bug reports and feature requests: hello@fleetless.dev.
376
105
 
377
- Fleetless is in closed beta. The waiting list is at
378
- <https://fleetless.dev/#waiting-list>.
106
+ Questions, bug reports and feature requests: hello@fleetless.dev.
379
107
 
380
- ## Reporting a security issue
108
+ ## 🔒 Reporting a security issue
381
109
 
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.
110
+ Email **security@fleetless.dev** rather than opening a public issue.
111
+ [SECURITY.md](SECURITY.md) says what is in scope for this package — tokens,
112
+ the PKCE verifier, the `state` value — and what belongs to the platform.
386
113
 
387
- ## Contributing
114
+ ## 🤝 Contributing
388
115
 
389
116
  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.
117
+ to <hello@fleetless.dev>, after reading [CONTRIBUTING.md](CONTRIBUTING.md) for
118
+ the setup, the checks and the Contributor Licence Agreement.
396
119
 
397
120
  By participating you agree to the [Code of Conduct](CODE_OF_CONDUCT.md).
398
121
 
399
- ## Maintainers
400
-
401
- Maintained by [Dehne Robotik GmbH](https://dehne-robotik.de).
122
+ ## 📜 Licence
402
123
 
403
- MIT licensed — see [LICENSE](LICENSE).
124
+ MIT — see [LICENSE](LICENSE). Maintained by
125
+ [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