@fleetless/sdk 2.0.2 → 3.0.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 +108 -0
- package/README.md +105 -59
- package/dist/index.cjs +4796 -1240
- package/dist/index.d.cts +881 -756
- package/dist/index.d.ts +881 -756
- package/dist/index.js +4796 -1240
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,114 @@ All notable changes to `@fleetless/sdk`. The format follows Keep a Changelog; th
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [3.0.0] — 2026-09-06
|
|
8
|
+
|
|
9
|
+
Fleetless shows an app user **no page**. The developer's own UI owns every
|
|
10
|
+
screen — login, registration, verification, invitation acceptance, password
|
|
11
|
+
reset, the provider buttons, the MCP consent — and this release replaces the
|
|
12
|
+
SDK's auth surface with the JSON API those screens call. The hosted,
|
|
13
|
+
app-branded login and consent pages are gone from the platform, so they are
|
|
14
|
+
gone from here.
|
|
15
|
+
|
|
16
|
+
### Removed
|
|
17
|
+
|
|
18
|
+
- **`auth.beginHostedLogin` and `auth.completeHostedLogin`**, with
|
|
19
|
+
`BeginHostedLoginOptions`, `HostedLoginRequest` and
|
|
20
|
+
`CompleteHostedLoginOptions`. The `/oauth/authorize` + `/oauth/token` flow
|
|
21
|
+
they drove no longer runs for app users. Use `auth.beginOidcLogin` and
|
|
22
|
+
`auth.completeOidcLogin` for federated sign-in, or `auth.login` for a
|
|
23
|
+
password.
|
|
24
|
+
- **`client.grants`** (`GrantsApi`, `grants.list`, `grants.revoke`), with the
|
|
25
|
+
`ConsentGrantSummary` and `ConsentRevokeResponse` re-exports. The routes
|
|
26
|
+
behind it are gone. The MCP consents an app user can withdraw are now
|
|
27
|
+
`auth.listMcpGrants()` and `auth.revokeMcpGrant(clientId)`.
|
|
28
|
+
- **`auth.passwordResetUrl()`**. It pointed at a Fleetless-served page that is
|
|
29
|
+
now the developer console's own, for Fleetless users rather than app users.
|
|
30
|
+
An app user's reset is `auth.requestPasswordReset` and
|
|
31
|
+
`auth.confirmPasswordReset`, in your own UI.
|
|
32
|
+
- **`LogoutResult`**, and with it everything `logout()` used to report about
|
|
33
|
+
the identity provider behind a session. That apparatus belonged to the
|
|
34
|
+
hosted login, where Fleetless owned the browser; an app that wants to end a
|
|
35
|
+
provider session redirects there itself, knowing its own provider.
|
|
36
|
+
- **`no_hosted_login_attempt`** from `SDK_ERROR_CODES`. The case it named —
|
|
37
|
+
nothing persisted for this attempt — still throws, as `state_mismatch`, and
|
|
38
|
+
the message still says which of the two happened.
|
|
39
|
+
- `HttpClient.requestOAuth`, the SDK's only form-urlencoded call. Nothing
|
|
40
|
+
reaches `/oauth/token` from here any more.
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
- **Registration and verification**: `auth.register({ email, password,
|
|
45
|
+
displayName? })`, `auth.verifyEmail(token)`,
|
|
46
|
+
`auth.resendVerification(email)`.
|
|
47
|
+
- **Recovery and invitations**: `auth.requestPasswordReset(email)`,
|
|
48
|
+
`auth.confirmPasswordReset(token, newPassword)`,
|
|
49
|
+
`auth.acceptInvitation({ token, password, displayName? })`. Every one of
|
|
50
|
+
these that spends a mailed token stores the session it answers with, so a
|
|
51
|
+
person is not asked to sign in again immediately after proving they can read
|
|
52
|
+
the mail.
|
|
53
|
+
- **Federated sign-in, per app**: `auth.listProviders()` for the buttons,
|
|
54
|
+
`auth.beginOidcLogin({ slug, redirectUri })` to build the start URL with
|
|
55
|
+
PKCE, `auth.completeOidcLogin({ code, state, expectedState, codeVerifier })`
|
|
56
|
+
to trade the one-time code for a session, and
|
|
57
|
+
`auth.oidcErrorFromCallback(params)` to read a failed sign-in off the
|
|
58
|
+
redirect back as a `FleetlessError`. The SDK builds the URL and **never
|
|
59
|
+
navigates**; the app does.
|
|
60
|
+
- **The MCP consent screen**: `auth.mcpInteraction(id)`,
|
|
61
|
+
`auth.approveMcpInteraction(id)`, `auth.denyMcpInteraction(id)`. Both
|
|
62
|
+
decisions answer where to send the browser, a denial included.
|
|
63
|
+
- **The app user's own standing consents**: `auth.listMcpGrants()` and
|
|
64
|
+
`auth.revokeMcpGrant(clientId)`.
|
|
65
|
+
- New option and result types: `RegisterOptions`, `AcceptInvitationOptions`,
|
|
66
|
+
`ProviderButton`, `BeginOidcLoginOptions`, `OidcLoginRequest`,
|
|
67
|
+
`CompleteOidcLoginOptions`, `McpInteractionDecision`. New wire re-exports:
|
|
68
|
+
`ClientMcpInteraction`, `McpConsentGrant`, `ClientOidcErrorCode`.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- **`ClientIdentity` renamed two fields**: `kind: 'end_user'` is now
|
|
73
|
+
`'app_user'`, and `end_user_id` is now `app_user_id`. A rename rather than a
|
|
74
|
+
kept key, deliberately — the old subject was a member of the org's one pool,
|
|
75
|
+
the new one is a row belonging to exactly one app, and a consumer reading
|
|
76
|
+
`.end_user_id` would have typechecked while meaning something subtly
|
|
77
|
+
different.
|
|
78
|
+
- **`logout()` resolves with `void`**, not a `LogoutResult`. It still never
|
|
79
|
+
rejects and still always clears the local store.
|
|
80
|
+
- **A `serverKey` client refuses session methods with a `FleetlessError` whose
|
|
81
|
+
code is `invalid_option`**, where it used to throw a bare `Error` a caller
|
|
82
|
+
could only catch by message. `me()`, `listProviders()`, `mcpInteraction()`
|
|
83
|
+
and `oidcErrorFromCallback()` still work on one: the first is what a server
|
|
84
|
+
key is for, the next two are reads the cloud answers without a credential,
|
|
85
|
+
and the last touches no network.
|
|
86
|
+
- `state_mismatch` is now thrown by `completeOidcLogin`, before any request.
|
|
87
|
+
- Pinned to `@fleetless/contracts` `48acd56`.
|
|
88
|
+
|
|
89
|
+
### Verification
|
|
90
|
+
|
|
91
|
+
- The `auth` and OIDC suites now drive the SDK's **default** `fetch` against a
|
|
92
|
+
real `node:http` server (`test/local-api.ts`) rather than a `vi.fn()`. Every
|
|
93
|
+
assertion about a path, a method, a header or a body is made against bytes
|
|
94
|
+
that actually left the process — which is what shows, for instance, that the
|
|
95
|
+
two MCP decisions send no `content-type` on their bodyless `POST`, a header
|
|
96
|
+
a fake `fetch` records as absent exactly as convincingly when it is wrong.
|
|
97
|
+
|
|
98
|
+
## [2.1.0] — 2026-09-04
|
|
99
|
+
|
|
100
|
+
### Added
|
|
101
|
+
|
|
102
|
+
- `InvokeOptions` is exported — the type of `actions.invoke`'s options
|
|
103
|
+
argument, which a caller constructs and could not name.
|
|
104
|
+
|
|
105
|
+
### Changed
|
|
106
|
+
|
|
107
|
+
- Every exported member carries a doc comment; the generated SDK reference
|
|
108
|
+
on docs.fleetless.dev is rendered from them.
|
|
109
|
+
- `InMemoryTokenStore` declares an explicit empty `constructor()`. It exists
|
|
110
|
+
only to carry a doc comment: TypeDoc reflects a synthesized constructor
|
|
111
|
+
for a class without one, and an undocumented member is what the reference
|
|
112
|
+
is built to have none of. Behaviour is unchanged — the class had an
|
|
113
|
+
implicit no-argument constructor before and has an explicit one now.
|
|
114
|
+
|
|
7
115
|
## [2.0.2] — 2026-09-04
|
|
8
116
|
|
|
9
117
|
No code change; the published `dist/` is byte-identical to 2.0.0.
|
package/README.md
CHANGED
|
@@ -9,8 +9,8 @@ The official TypeScript SDK for apps built on [Fleetless](https://fleetless.dev)
|
|
|
9
9
|
which exposes a ROS 2 robot as a hosted REST and realtime API — exactly the
|
|
10
10
|
topics, services and actions a developer chose to publish, under stable slugs,
|
|
11
11
|
behind roles. The SDK covers all of it: datapoints (read, subscribe, recorded
|
|
12
|
-
history), actions, services, publishers, cameras, jobs, assets and URDF,
|
|
13
|
-
|
|
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
14
|
ships ESM and CJS with its own types, and runs wherever a `fetch` and a
|
|
15
15
|
`WebSocket` exist — a browser, a mobile webview, Node 20 or newer, and
|
|
16
16
|
server-side with a server key instead of a user session.
|
|
@@ -40,11 +40,13 @@ server-side with a server key instead of a user session.
|
|
|
40
40
|
`cameras.live` takes a refcounted hold: the first viewer starts the robot
|
|
41
41
|
publishing, the last one leaving stops it. You get a LiveKit room URL and
|
|
42
42
|
token to hand to the LiveKit client of your choice.
|
|
43
|
-
- **
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
- **The whole auth surface, and every screen stays yours.** Fleetless renders
|
|
44
|
+
no page for an app user: registration with email verification, invitations,
|
|
45
|
+
password reset, single sign-on to one of the app's identity providers, and
|
|
46
|
+
the MCP consent screen are all methods on `client.auth`, called from a UI you
|
|
47
|
+
wrote. The federated flow runs its own PKCE (S256 only) against Fleetless;
|
|
48
|
+
`beginOidcLogin` makes no network call, and `completeOidcLogin` checks
|
|
49
|
+
`state` before it exchanges anything.
|
|
48
50
|
- **A token store you control.** Implement two methods to keep a session in
|
|
49
51
|
`localStorage`, a cookie or a native keystore; the default keeps it in
|
|
50
52
|
memory. Refresh is silent and single-flight, so ten concurrent calls that
|
|
@@ -56,8 +58,10 @@ server-side with a server key instead of a user session.
|
|
|
56
58
|
npm i @fleetless/sdk
|
|
57
59
|
```
|
|
58
60
|
|
|
59
|
-
You need an app identifier and an
|
|
60
|
-
|
|
61
|
+
You need an app identifier and the credentials of an **app user** — an
|
|
62
|
+
account inside that app, which the developer's team creates or invites from
|
|
63
|
+
the [Fleetless Console](https://console.fleetless.dev). A console login is a
|
|
64
|
+
Fleetless user, a different identity space, and will not sign in here.
|
|
61
65
|
|
|
62
66
|
```ts checked
|
|
63
67
|
import { createClient } from '@fleetless/sdk'
|
|
@@ -98,44 +102,106 @@ Slugs are the names you chose in the console, in `snake_case`
|
|
|
98
102
|
(`[a-z][a-z0-9]*(_[a-z0-9]+)*`) — never the robot's internal ROS names, which
|
|
99
103
|
is what makes renaming a node on the robot invisible to your app.
|
|
100
104
|
|
|
101
|
-
###
|
|
105
|
+
### Your own sign-in UI
|
|
102
106
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
Every screen an app user sees belongs to your app. These are the calls behind
|
|
108
|
+
them; the full walkthrough, page by page, is the
|
|
109
|
+
[app-auth recipe](https://docs.fleetless.dev/recipes/app-auth/).
|
|
106
110
|
|
|
107
111
|
```ts
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
112
|
+
// Registration. Resolves on 202 — which says a policy-allowed request was
|
|
113
|
+
// accepted, NOT that an account was created. An address the app already knows
|
|
114
|
+
// gets the identical answer with no mail sent, so the only honest thing to
|
|
115
|
+
// render is a sentence about the mailbox.
|
|
116
|
+
await client.auth.register({ email, password, displayName })
|
|
117
|
+
|
|
118
|
+
// The page your app serves at the app's configured `verify_url`. Stores a
|
|
119
|
+
// session, so nobody types a password right after reading their mail.
|
|
120
|
+
await client.auth.verifyEmail(tokenFromTheLink)
|
|
121
|
+
|
|
122
|
+
// Recovery. requestPasswordReset always resolves on 202, for a known address
|
|
123
|
+
// and an unknown one alike.
|
|
124
|
+
await client.auth.requestPasswordReset(email)
|
|
125
|
+
await client.auth.confirmPasswordReset(tokenFromTheLink, newPassword)
|
|
126
|
+
|
|
127
|
+
// The page your app serves at the app's configured `invite_url`.
|
|
128
|
+
await client.auth.acceptInvitation({ token: tokenFromTheLink, password })
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Sign-in through an identity provider
|
|
132
|
+
|
|
133
|
+
An app may carry any number of OpenID Connect providers, configured per app in
|
|
134
|
+
the console. Your page draws the buttons; your app never talks to the provider.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
// Draw the buttons. Only enabled providers are listed, slug and name only.
|
|
138
|
+
const providers = await client.auth.listProviders()
|
|
139
|
+
|
|
140
|
+
// 1. Build the URL. No network call: this generates the PKCE verifier and the
|
|
141
|
+
// state, and hands them back, because a redirect is a fresh page load.
|
|
142
|
+
const request = await client.auth.beginOidcLogin({
|
|
143
|
+
slug: 'azure',
|
|
111
144
|
redirectUri: 'https://your-app.example.com/callback',
|
|
112
145
|
})
|
|
113
|
-
|
|
114
|
-
// A redirect is a fresh page load, so persist these two yourself.
|
|
115
146
|
sessionStorage.setItem('fleetless_state', request.state)
|
|
116
147
|
sessionStorage.setItem('fleetless_code_verifier', request.codeVerifier)
|
|
117
|
-
window.location.
|
|
118
|
-
|
|
119
|
-
// 2. On the callback page.
|
|
120
|
-
//
|
|
121
|
-
const params = new
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
148
|
+
window.location.assign(request.url)
|
|
149
|
+
|
|
150
|
+
// 2. On the callback page. A refused sign-in redirects here with `error`
|
|
151
|
+
// rather than a code, so check for that first.
|
|
152
|
+
const params = new URL(window.location.href).searchParams
|
|
153
|
+
const failure = client.auth.oidcErrorFromCallback(params)
|
|
154
|
+
if (failure) {
|
|
155
|
+
// A FleetlessError carrying the documented reason: no_access, email_taken,
|
|
156
|
+
// email_unverified, domain_not_allowed, registration_closed, and the rest.
|
|
157
|
+
showSignInProblem(failure)
|
|
158
|
+
} else {
|
|
159
|
+
await client.auth.completeOidcLogin({
|
|
127
160
|
code: params.get('code') ?? '',
|
|
128
161
|
state: params.get('state') ?? '',
|
|
129
|
-
expectedState,
|
|
130
|
-
codeVerifier,
|
|
131
|
-
clientId: 'oauth-client-id-from-the-console',
|
|
132
|
-
redirectUri: 'https://your-app.example.com/callback',
|
|
162
|
+
expectedState: sessionStorage.getItem('fleetless_state') ?? '',
|
|
163
|
+
codeVerifier: sessionStorage.getItem('fleetless_code_verifier') ?? '',
|
|
133
164
|
})
|
|
134
165
|
// From here on me(), logout() and silent refresh behave exactly as they do
|
|
135
|
-
// after login().
|
|
166
|
+
// after login().
|
|
136
167
|
}
|
|
137
168
|
```
|
|
138
169
|
|
|
170
|
+
`completeOidcLogin` refuses before any network call, with `state_mismatch`,
|
|
171
|
+
for a mismatch, a missing `state`, **and** an empty `expectedState` — that
|
|
172
|
+
last case is checked first and explicitly, because two empty strings compare
|
|
173
|
+
equal.
|
|
174
|
+
|
|
175
|
+
One callback URL is registered at the provider,
|
|
176
|
+
`https://api.fleetless.dev/api/client/oidc/callback`, the same for every app
|
|
177
|
+
and every provider. It is not your `redirectUri`, which is a page in your app.
|
|
178
|
+
|
|
179
|
+
### The MCP consent screen
|
|
180
|
+
|
|
181
|
+
Only if the app serves its own MCP endpoint. An authorization redirects the
|
|
182
|
+
browser to a page your app serves, with an interaction id; your page signs the
|
|
183
|
+
person in, shows them what is being asked, and answers.
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// Sign the person in FIRST — `already_granted` is derived from the bearer,
|
|
187
|
+
// and an anonymous read answers `false` for everybody.
|
|
188
|
+
const interaction = await client.auth.mcpInteraction(interactionId)
|
|
189
|
+
|
|
190
|
+
// `interaction.client_name` is a name the client typed about itself during an
|
|
191
|
+
// unauthenticated registration. `client_name_verified` is the literal false;
|
|
192
|
+
// render it as a claim, never as an identity. `scopes` is always empty: what a
|
|
193
|
+
// session reaches is decided by the user's role, re-read on every call.
|
|
194
|
+
const { redirectTo } = approved
|
|
195
|
+
? await client.auth.approveMcpInteraction(interactionId)
|
|
196
|
+
: await client.auth.denyMcpInteraction(interactionId)
|
|
197
|
+
window.location.assign(redirectTo) // a denial redirects too
|
|
198
|
+
|
|
199
|
+
// A "connected apps" screen. Withdrawing stops the NEXT authorization; a
|
|
200
|
+
// session already running keeps working for the rest of its 15 minutes.
|
|
201
|
+
const grants = await client.auth.listMcpGrants()
|
|
202
|
+
await client.auth.revokeMcpGrant(grants[0].client_id)
|
|
203
|
+
```
|
|
204
|
+
|
|
139
205
|
### Running a job
|
|
140
206
|
|
|
141
207
|
An action is a job with a lifecycle. `invoke` resolves as soon as the job
|
|
@@ -184,30 +250,6 @@ await room.disconnect()
|
|
|
184
250
|
await session.release()
|
|
185
251
|
```
|
|
186
252
|
|
|
187
|
-
## Migrating from 1.0.0
|
|
188
|
-
|
|
189
|
-
2.0.0 is a breaking release. `CHANGELOG.md` carries the complete inventory —
|
|
190
|
-
established by diffing the published 1.0.0 tarball's types against this one's,
|
|
191
|
-
not read off the commit log. The breaks, one line each:
|
|
192
|
-
|
|
193
|
-
- `auth.register()` and `auth.confirmRegistration()` are gone with no
|
|
194
|
-
successor; a group grows by invitation (see [Account recovery](https://docs.fleetless.dev/reference/sdk/#account-recovery) in the SDK reference).
|
|
195
|
-
- `auth.requestPasswordReset()` and `auth.confirmPasswordReset()` are gone.
|
|
196
|
-
Link the user to `auth.passwordResetUrl()` instead.
|
|
197
|
-
- The types `MailStatus` and `ClientRegisterResponse` went with them.
|
|
198
|
-
- The error code `not_a_member` is gone — a compile break for an exhaustive
|
|
199
|
-
`switch` on `error.code`.
|
|
200
|
-
- `CameraDescriptor.snapshot_interval_ms` is now `snapshot_interval_seconds`.
|
|
201
|
-
**Read that twice:** it is a rename *and* a unit change, so renaming the
|
|
202
|
-
property and nothing else leaves you with a number a thousand times too
|
|
203
|
-
small.
|
|
204
|
-
- `UrdfCompleteness.missing` and `UrdfSceneResources.missing` are no longer
|
|
205
|
-
`string[]` but `{ uri, element }[]`, so a missing texture is no longer
|
|
206
|
-
reported as a missing mesh. For the old shape: `missing.map((m) => m.uri)`.
|
|
207
|
-
|
|
208
|
-
Nothing else was removed or re-typed. `auth.logout()`'s result grew a field and
|
|
209
|
-
gained a name (`LogoutResult`); `.revoked` is unchanged.
|
|
210
|
-
|
|
211
253
|
## Documentation and help
|
|
212
254
|
|
|
213
255
|
- **[SDK reference](https://docs.fleetless.dev/reference/sdk/)** — every method,
|
|
@@ -216,8 +258,12 @@ gained a name (`LogoutResult`); `.revoked` is unchanged.
|
|
|
216
258
|
robot that has never connected to a value in your app.
|
|
217
259
|
- **[REST and realtime API](https://docs.fleetless.dev/reference/api/)** — the
|
|
218
260
|
wire surface underneath this package.
|
|
219
|
-
- **[
|
|
220
|
-
|
|
261
|
+
- **[Your own login UI](https://docs.fleetless.dev/recipes/app-auth/)** — the
|
|
262
|
+
full walkthrough for every auth screen, and the mistakes that cost the most
|
|
263
|
+
time.
|
|
264
|
+
- **[Identity](https://docs.fleetless.dev/reference/identity/)** — the two
|
|
265
|
+
identity spaces, the federation rules, and what each refusal licenses your
|
|
266
|
+
UI to claim.
|
|
221
267
|
- **[CHANGELOG.md](CHANGELOG.md)** — what changed in each version.
|
|
222
268
|
- Questions, bug reports and feature requests: hello@fleetless.dev.
|
|
223
269
|
|