@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/CHANGELOG.md +19 -0
- package/CONTRIBUTING.md +76 -71
- package/README.md +66 -344
- package/SECURITY.md +11 -11
- package/dist/index.cjs +311 -209
- package/dist/index.d.cts +331 -94
- package/dist/index.d.ts +331 -94
- package/dist/index.js +311 -209
- package/package.json +9 -5
- package/RELEASING.md +0 -195
package/README.md
CHANGED
|
@@ -5,82 +5,51 @@
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
developer picks which
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
70
|
+
// Live updates: the current value first, then every change.
|
|
103
71
|
const subscription = client.datapoints.subscribe(robotId, 'battery_percentage', {
|
|
104
|
-
onEvent(event)
|
|
105
|
-
|
|
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
|
-
`
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
370
|
-
time.
|
|
99
|
+
walkthrough for every sign-in screen.
|
|
371
100
|
- **[Identity](https://docs.fleetless.dev/reference/identity/)** — the two
|
|
372
|
-
identity spaces,
|
|
373
|
-
|
|
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
|
-
|
|
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
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
391
|
-
|
|
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
|
-
##
|
|
400
|
-
|
|
401
|
-
Maintained by [Dehne Robotik GmbH](https://dehne-robotik.de).
|
|
122
|
+
## 📜 Licence
|
|
402
123
|
|
|
403
|
-
MIT
|
|
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
|
|
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
|
-
|
|
43
|
-
your decision and
|
|
44
|
-
|
|
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
|
|
49
|
-
to you
|
|
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`,
|
|
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
|
|
83
|
-
a change to this package. Report
|
|
84
|
-
|
|
85
|
-
|
|
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
|