@fleetless/sdk 2.1.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,16 +1,19 @@
1
+ // SPDX-License-Identifier: MIT
1
2
  import { z } from 'zod';
2
3
 
4
+ // SPDX-License-Identifier: Apache-2.0
5
+
3
6
  /**
4
- * Jobs (spec §6.1, §11.3): one running unit of work on a robot — an action
7
+ * Jobs: one running unit of work on a robot — an action
5
8
  * goal or a service call — with an id both sides know, so bridge and cloud
6
9
  * stay in sync across a disconnect.
7
10
  *
8
11
  * Two rules shape everything here:
9
12
  *
10
- * 1. **State is observed by slug, not by id.** The id is informative (§11.3);
11
- * a client watches `robot × slug` and sees whatever job is running there,
12
- * which is also why every observer of a slug sees the same job.
13
- * 2. **`lost` is a real outcome and must be said out loud** (§6.1). Job state
13
+ * 1. **State is observed by slug, not by id.** The id is informative; a client
14
+ * watches `robot × slug` and sees whatever job is running there, which is
15
+ * also why every observer of a slug sees the same job.
16
+ * 2. **`lost` is a real outcome and must be said out loud.** Job state
14
17
  * lives only in the bridge's memory; if it restarts mid-job, the results
15
18
  * are gone. The cloud then marks the job `lost` — never leaves it reading
16
19
  * "running" because nobody contradicted it. A system that reports a
@@ -18,9 +21,9 @@ import { z } from 'zod';
18
21
  * admits it lost track.
19
22
  */
20
23
  declare const jobState: z.ZodEnum<{
24
+ failed: "failed";
21
25
  running: "running";
22
26
  succeeded: "succeeded";
23
- failed: "failed";
24
27
  cancelled: "cancelled";
25
28
  lost: "lost";
26
29
  }>;
@@ -30,9 +33,9 @@ declare const job: z.ZodObject<{
30
33
  robot_id: z.ZodUUID;
31
34
  slug: z.ZodString;
32
35
  state: z.ZodEnum<{
36
+ failed: "failed";
33
37
  running: "running";
34
38
  succeeded: "succeeded";
35
- failed: "failed";
36
39
  cancelled: "cancelled";
37
40
  lost: "lost";
38
41
  }>;
@@ -50,8 +53,8 @@ type Job = z.infer<typeof job>;
50
53
  /**
51
54
  * One update about a job, pushed to subscribers of its slug.
52
55
  *
53
- * `timestamp_ms` is the bridge's capture time, exactly as for a datapoint
54
- * (§6.3 says action feedback carries it too) — so a client computes the age
56
+ * `timestamp_ms` is the bridge's capture time, exactly as for a datapoint —
57
+ * action feedback carries it too — so a client computes the age
55
58
  * of a progress report the same way it computes the age of a sensor value,
56
59
  * and a burst of late-delivered feedback after a reconnect is visibly late
57
60
  * rather than looking current.
@@ -65,9 +68,9 @@ declare const jobEvent: z.ZodObject<{
65
68
  robot_id: z.ZodUUID;
66
69
  slug: z.ZodString;
67
70
  state: z.ZodEnum<{
71
+ failed: "failed";
68
72
  running: "running";
69
73
  succeeded: "succeeded";
70
- failed: "failed";
71
74
  cancelled: "cancelled";
72
75
  lost: "lost";
73
76
  }>;
@@ -87,9 +90,8 @@ declare const jobEvent: z.ZodObject<{
87
90
  }, z.core.$strip>;
88
91
  type JobEvent = z.infer<typeof jobEvent>;
89
92
  /**
90
- * What a busy refusal tells the caller (spec §11.3: "inkl. Information, was
91
- * läuft"). A refusal that only says "busy" forces the caller to guess whether
92
- * to wait or to give up.
93
+ * What a busy refusal tells the caller: what is already running. A refusal that
94
+ * only says "busy" forces the caller to guess whether to wait or to give up.
93
95
  */
94
96
  declare const busyDetails: z.ZodObject<{
95
97
  running: z.ZodObject<{
@@ -97,9 +99,9 @@ declare const busyDetails: z.ZodObject<{
97
99
  robot_id: z.ZodUUID;
98
100
  slug: z.ZodString;
99
101
  state: z.ZodEnum<{
102
+ failed: "failed";
100
103
  running: "running";
101
104
  succeeded: "succeeded";
102
- failed: "failed";
103
105
  cancelled: "cancelled";
104
106
  lost: "lost";
105
107
  }>;
@@ -118,7 +120,7 @@ type BusyDetails = z.infer<typeof busyDetails>;
118
120
 
119
121
  /**
120
122
  * The REST read of one datapoint. For bridge-captured data `timestamp_ms`
121
- * is the capture time at the bridge (spec §6.3); for the cloud-observed
123
+ * is the capture time at the bridge; for the cloud-observed
122
124
  * built-in `bridge_state` it is the time the cloud observed the state.
123
125
  */
124
126
  declare const datapointValue: z.ZodObject<{
@@ -129,20 +131,18 @@ declare const datapointValue: z.ZodObject<{
129
131
  type DatapointValue = z.infer<typeof datapointValue>;
130
132
  /**
131
133
  * Every job the platform currently believes this robot has — `GET
132
- * /api/robots/:id/jobs` (W6b).
134
+ * /api/robots/:id/jobs`.
133
135
  *
134
136
  * `jobResponse` answers "what is on this slug", which requires knowing the
135
- * slug first. That was enough while a job could only exist on a slug the
136
- * published configuration named. W6b breaks that assumption twice: a
137
- * reconnecting bridge can name a job the cloud has **no row for** and the
138
- * cloud adopts it, and a configuration change can leave a job on a slug the
139
- * document no longer contains. Both are jobs nobody can ask about, because
140
- * asking requires already knowing what to ask for.
137
+ * slug first. Two kinds of job break that assumption: a reconnecting bridge
138
+ * can name a job the cloud has **no row for**, and the cloud adopts it; and a
139
+ * configuration change can leave a job on a slug the document no longer
140
+ * contains. Both are jobs nobody can ask about, because asking requires
141
+ * already knowing what to ask for.
141
142
  *
142
- * So this route exists to answer the question the per-slug route cannot: not
143
- * "is something running here", but "what is this robot doing". A restarted
144
- * cloud that has just reconciled a robot's `hello.active_jobs` has exactly
145
- * this list and, until now, no way to say it out loud.
143
+ * So this route answers the question the per-slug route cannot: not "is
144
+ * something running here", but "what is this robot doing". A cloud that has
145
+ * just reconciled a robot's `hello.active_jobs` has exactly this list.
146
146
  *
147
147
  * The array is ordered newest first and is **never null**: a robot doing
148
148
  * nothing answers `{ jobs: [] }`. "Nothing is running" and "we did not look"
@@ -150,14 +150,11 @@ type DatapointValue = z.infer<typeof datapointValue>;
150
150
  * distinction `robotDeletionSummary` was made all-required for.
151
151
  *
152
152
  * **At most one entry per slug: the current job there, exactly what
153
- * `jobResponse` would answer for that slug.** This is not a history endpoint
154
- * and must not become one. The first implementation returned every job the
155
- * registry still held — six rows and four complete Fibonacci results after a
156
- * few minutes of gate traffic, and unbounded in both count and payload for a
157
- * robot that has been working all day. The list would have grown until a
158
- * console page carried a robot's entire past, and the one thing it exists to
159
- * answer — *what is this robot doing* — would have been the first line of a
160
- * scroll.
153
+ * `jobResponse` would answer for that slug.** This is not a history endpoint.
154
+ * Returning every job a registry still holds is unbounded in both count and
155
+ * payload for a robot that has been working all day, and the one thing this
156
+ * route exists to answer — *what is this robot doing* — would be the first
157
+ * line of a scroll. The durable history has its own routes.
161
158
  *
162
159
  * A settled job stays visible as its slug's current entry until something
163
160
  * else runs there, which is what makes a job that just failed still findable.
@@ -165,7 +162,7 @@ type DatapointValue = z.infer<typeof datapointValue>;
165
162
  * `jobResponse`.
166
163
  */
167
164
  /**
168
- * What a `rate_limited` refusal tells the caller (W6c).
165
+ * What a `rate_limited` refusal tells the caller.
169
166
  *
170
167
  * One number, and it is the only one that matters: **when to come back.** A
171
168
  * limit that says "too many" without saying "in 800 ms" produces a client that
@@ -189,7 +186,7 @@ declare const cameraDescriptor: z.ZodObject<{
189
186
  }, z.core.$strip>;
190
187
  type CameraDescriptor = z.infer<typeof cameraDescriptor>;
191
188
  /**
192
- * Raw samples. `timestamp_ms` is the **bridge's capture time** (§6.3) — the
189
+ * Raw samples. `timestamp_ms` is the **bridge's capture time** — the
193
190
  * same instant the live value carried, so a recorded point and a live one can
194
191
  * be placed on one axis without apology.
195
192
  *
@@ -217,9 +214,8 @@ type HistorySamplesResponse = z.infer<typeof historySamplesResponse>;
217
214
  * inspection.
218
215
  *
219
216
  * `sample_count` exists because an empty bucket and a bucket whose average is
220
- * zero are different facts. W5 established at some cost what happens when two
221
- * facts share one representation, and a chart is the easiest place in this
222
- * product to draw a gap as a line.
217
+ * zero are different facts. When two facts share one representation, a chart
218
+ * is the easiest place to draw a gap as a line.
223
219
  */
224
220
  declare const historyBucketsResponse: z.ZodObject<{
225
221
  slug: z.ZodString;
@@ -238,6 +234,8 @@ declare const historyBucketsResponse: z.ZodObject<{
238
234
  }, z.core.$strip>;
239
235
  type HistoryBucketsResponse = z.infer<typeof historyBucketsResponse>;
240
236
 
237
+ // SPDX-License-Identifier: Apache-2.0
238
+
241
239
  /**
242
240
  * One datapoint sample pushed to a subscriber. The current value arrives
243
241
  * immediately on subscribe, then every change. `timestamp_ms` semantics as
@@ -252,10 +250,15 @@ declare const datapointEvent: z.ZodObject<{
252
250
  }, z.core.$strip>;
253
251
  type DatapointEvent = z.infer<typeof datapointEvent>;
254
252
 
253
+ // SPDX-License-Identifier: Apache-2.0
254
+
255
255
  /**
256
- * Access plus refresh (spec §3.4). The access token is short-lived; the
256
+ * Access plus refresh. The access token is short-lived; the
257
257
  * refresh token rotates on every use, so a stolen one is detectable when the
258
258
  * original is presented again.
259
+ *
260
+ * One shape for both identity spaces: a session is a session, and the claims
261
+ * inside the token are what differ.
259
262
  */
260
263
  declare const sessionTokens: z.ZodObject<{
261
264
  access_token: z.ZodString;
@@ -264,109 +267,157 @@ declare const sessionTokens: z.ZodObject<{
264
267
  }, z.core.$strip>;
265
268
  type SessionTokens = z.infer<typeof sessionTokens>;
266
269
 
270
+ // SPDX-License-Identifier: Apache-2.0
271
+
267
272
  /**
268
- * **What logout can and cannot end, said in three separable facts (W9c,
269
- * DEF-098).**
273
+ * **Why a federated sign-in ended without a session, in a code the app can
274
+ * branch on** — carried back to the app's own `redirect_uri` as `error`, not
275
+ * rendered by Fleetless. The only Fleetless-rendered page in this flow is
276
+ * the one for a state that can no longer be resolved to a redirect URI, because
277
+ * then there is nowhere to send the answer.
278
+ *
279
+ * The five rows of D4's table are the first five values plus `no_access`:
270
280
  *
271
- * Until now this route answered `204`: the Fleetless session was over and the
272
- * response had nothing to say about the *other* session. For a federated user
273
- * that is the larger half — they clicked "log out", the IdP's cookie survived,
274
- * and the next login goes straight through without a password. The register
275
- * row calls that an expectation gap, and it is: the word on the button is
276
- * "log out", not "log out of this app".
281
+ * - `no_access` — the identity is unknown and nothing admits it, or the account
282
+ * it names is not `active`. **One code for both**, because to the person the
283
+ * remedy is the same — ask somebody to let you in — and a code that split an
284
+ * outcome nobody acts on differently would tell a stranger which half applied.
285
+ * - `email_taken` — the address already belongs to another app user, and the
286
+ * provider is not permitted to link (`link_verified_emails`, or the provider
287
+ * did not assert `email_verified`). Deliberately not `no_access`: the remedy
288
+ * is different — *sign in the way you signed up*.
289
+ * - `email_unverified` — the provider asserted an address without
290
+ * `email_verified`. **An unverified address never produces or links an
291
+ * account**, whatever the rest of the policy says.
292
+ * - `domain_not_allowed`, `registration_closed` — the self-registration policy
293
+ * refused. Honest, because neither is about whether a person exists.
294
+ * - `idp_unavailable`, `exchange_failed`, `claims_incomplete`,
295
+ * `provider_misconfigured`, `provider_disabled` — the provider's or the
296
+ * developer's to fix, and the app can say so.
297
+ * - `invalid_request` — the start parameters did not hold up.
298
+ * - `quota_exceeded` — the org has as many app users as its `max_end_users`
299
+ * quota allows, so no account can be created for this identity. Named rather
300
+ * than folded into `no_access`, for `domain_not_allowed`'s reason: it is not
301
+ * about the person, the app can say what happened, and the remedy belongs to
302
+ * the developer rather than to whoever is trying to sign in. It is raised
303
+ * **only where an account would be created** — an identity that already has
304
+ * one signs in at the quota exactly as it does under it, because refusing a
305
+ * sign-in would turn a protection limit into an outage.
306
+ */
307
+ declare const clientOidcErrorCode: z.ZodEnum<{
308
+ no_access: "no_access";
309
+ email_taken: "email_taken";
310
+ email_unverified: "email_unverified";
311
+ domain_not_allowed: "domain_not_allowed";
312
+ registration_closed: "registration_closed";
313
+ idp_unavailable: "idp_unavailable";
314
+ exchange_failed: "exchange_failed";
315
+ claims_incomplete: "claims_incomplete";
316
+ provider_misconfigured: "provider_misconfigured";
317
+ provider_disabled: "provider_disabled";
318
+ invalid_request: "invalid_request";
319
+ quota_exceeded: "quota_exceeded";
320
+ }>;
321
+ type ClientOidcErrorCode = z.infer<typeof clientOidcErrorCode>;
322
+ /**
323
+ * **A pending MCP authorization, as the app's own consent screen reads it**
324
+ * Fleetless renders no page here either: `authorize` redirects to the
325
+ * app's `mcp_login_url` with an interaction id, the app authenticates the user
326
+ * with its normal UI, shows this, and approves or denies through the API.
277
327
  *
278
- * **The Fleetless session is ended before this is computed, unconditionally.**
279
- * Nothing below can fail in a way that leaves the caller logged in here — a
280
- * logout that depends on reaching a third party is not a logout.
328
+ * `client_name_verified` is `z.literal(false)`, and that is the whole point of
329
+ * the field. The name comes from an **unauthenticated** dynamic registration —
330
+ * the client typed it about itself, nobody checked it — so a consent screen
331
+ * that rendered it as though it were an identity would be teaching people to
332
+ * trust a string an attacker chooses. A literal rather than a boolean because
333
+ * there is no verified case to distinguish: an app that reads this field at all
334
+ * has to handle the untrusted one, and a `true` branch would be dead code
335
+ * pretending to be a safeguard.
336
+ */
337
+ declare const clientMcpInteraction: z.ZodObject<{
338
+ id: z.ZodString;
339
+ app_id: z.ZodUUID;
340
+ client_name: z.ZodNullable<z.ZodString>;
341
+ client_name_verified: z.ZodLiteral<false>;
342
+ scopes: z.ZodArray<z.ZodString>;
343
+ already_granted: z.ZodBoolean;
344
+ expires_at: z.ZodISODateTime;
345
+ }, z.core.$strip>;
346
+ type ClientMcpInteraction = z.infer<typeof clientMcpInteraction>;
347
+ /**
348
+ * **One standing MCP consent, as both withdrawal doors list it.**
281
349
  *
282
- * **Four outcomes**, and they are deliberately not collapsed into a nullable
283
- * URL. *No IdP was involved* and *an IdP was involved and publishes no
284
- * `end_session_endpoint`* are different things: the first needs no action and
285
- * the second means a session survives that this platform cannot end. A caller
286
- * that renders them identically is choosing to; a contract that cannot tell
287
- * them apart makes the choice for everyone.
350
+ * A grant is what lets a later authorization skip the app's consent screen:
351
+ * `clientMcpInteraction.already_granted` is a read of exactly this row. It is
352
+ * written when a person approves and it is removed by neither the client's
353
+ * registration lapsing nor its access token expiring — so without a door it
354
+ * was a decision a person could make once and never unmake.
288
355
  *
289
- * **This sentence said "three" for a whole wave, directly above a four-branch
290
- * union in this same file** — found by Momus-W9, along with the same number in
291
- * `sdk/src/auth.ts` and `sdk/README.md`. The type is derived
292
- * (`ClientLogoutResponse['idp_logout']`), so `tsc` had nothing to say, and the
293
- * sweep shows the mechanism plainly: `sdk d065447` is literally titled *"logout
294
- * says three separable things"* — correct when written, never carried forward
295
- * when `hint_unavailable` arrived in `b417d2a`.
356
+ * **Standing only.** A withdrawn grant is stamped rather than deleted, so the
357
+ * store still holds it; neither listing returns one. The question both doors
358
+ * ask is *what is connected right now*, and a row that answered "connected,
359
+ * but no" would be a state every caller has to filter for itself.
296
360
  *
297
- * The count is not the point. **`hint_unavailable` is precisely the case this
298
- * comment warns about** — the IdP session survives — so a caller who handles
299
- * the three documented branches drops it into an `else` they believe means
300
- * *nothing to do*.
361
+ * `client_name_verified` is `z.literal(false)` for the reason
362
+ * `clientMcpInteraction` gives at length: the name comes from an
363
+ * unauthenticated dynamic registration, the client chose it about itself, and
364
+ * a list that rendered it as an identity would be teaching people to trust a
365
+ * string an attacker picked. Here it matters more than on the consent screen,
366
+ * not less — a "connected apps" list is read long after the moment of
367
+ * approval, when nobody remembers what they clicked.
301
368
  */
302
- declare const clientLogoutResponse: z.ZodObject<{
303
- idp_logout: z.ZodDiscriminatedUnion<[z.ZodObject<{
304
- status: z.ZodLiteral<"redirect">;
305
- url: z.ZodURL;
306
- }, z.core.$strip>, z.ZodObject<{
307
- status: z.ZodLiteral<"not_federated">;
308
- }, z.core.$strip>, z.ZodObject<{
309
- status: z.ZodLiteral<"unsupported_by_idp">;
310
- }, z.core.$strip>, z.ZodObject<{
311
- status: z.ZodLiteral<"hint_unavailable">;
312
- }, z.core.$strip>, z.ZodObject<{
313
- status: z.ZodLiteral<"session_unknown">;
314
- }, z.core.$strip>], "status">;
369
+ declare const mcpConsentGrant: z.ZodObject<{
370
+ client_id: z.ZodString;
371
+ client_name: z.ZodNullable<z.ZodString>;
372
+ client_name_verified: z.ZodLiteral<false>;
373
+ granted_at: z.ZodISODateTime;
315
374
  }, z.core.$strip>;
316
- type ClientLogoutResponse = z.infer<typeof clientLogoutResponse>;
375
+ type McpConsentGrant = z.infer<typeof mcpConsentGrant>;
317
376
  /**
318
377
  * Who the caller turned out to be. Returned by the "who am I" endpoint and by
319
378
  * the realtime `auth_ok` frame, so a client can render a session without
320
379
  * decoding a token itself — decoding a JWT in the client is how apps end up
321
380
  * trusting claims nobody verified.
322
381
  *
323
- * **Three kinds of caller reach the client API, not two.** Besides end users
324
- * and server keys, a **developer** does: spec §15.2 says the console's
325
- * playground runs over the real client API and appears in the audit as the
326
- * developer, and the console's own live views (robot list badges, the Live
327
- * tab) subscribe on `/realtime` as one. A developer is **org-scoped, not
382
+ * **Three kinds of caller reach the client API.** Besides app users and server
383
+ * keys, a **developer** does: the console's playground runs over the real
384
+ * client API and appears in the audit as the developer, and the console's own
385
+ * live views subscribe on `/realtime` as one. A developer is **org-scoped, not
328
386
  * app-scoped** — they own the configuration of every robot in their org — so
329
387
  * `app_id` and `role_id` are null for them, and roles do not filter what they
330
- * see. `kind` states this explicitly rather than leaving it to be inferred
331
- * from which id happens to be set.
388
+ * see. `kind` states this explicitly rather than leaving it to be inferred from
389
+ * which id happens to be set.
332
390
  *
333
- * **`act` is the real admin behind an impersonation** (spec
334
- * `2026-08-29-org-identity-redesign`, D4, the `act`-claim pattern of RFC
335
- * 8693). An Org Admins member entering an app through the interstitial
336
- * (`impersonationChoice`) gets a token whose *effective* identity is the role
337
- * or user they chose — that is what the rest of this shape describes — while
338
- * `act` names **the admin who is actually driving**. So every action can
339
- * audit as "Admin A as User B / as role X", and a client can render the "you
340
- * are acting as …" banner without decoding the token.
391
+ * **`end_user_id` became `app_user_id`, and that is a rename with a meaning.**
392
+ * The old subject was a member of the org's one pool, reachable through an
393
+ * assignment; the new one is a row that belongs to exactly one app. Renaming
394
+ * rather than keeping the key is deliberate: a consumer reading `.end_user_id`
395
+ * would have typechecked and meant something subtly different, which is the
396
+ * quietest way for a cut like this to go wrong.
341
397
  *
342
- * **Optional, not a nullable actor, for `orgUser.tier`'s reason:** absence
343
- * means *this is an ordinary session, nobody is delegating*, which is not the
344
- * same fact as *the actor is unknown*. The overwhelming majority of sessions
345
- * are ordinary and carry no `act` at all; a session that has one is a
346
- * delegation and says who by. The schema cannot check that `act` is present
347
- * exactly when the effective identity was impersonated — that pairing is the
348
- * cloud's, minted at the authorize step. Only the admin's **id** rides here:
349
- * the label is resolved by whoever renders it, not carried as a second
350
- * unverified name on the wire.
398
+ * **`act` is gone.** It named the org admin behind an impersonation (the RFC
399
+ * 8693 pattern). Impersonation is deleted with no successor, so a field
400
+ * that could still arrive would describe a delegation nothing can mint — and a
401
+ * client rendering "you are acting as …" from it would be showing a state the
402
+ * platform cannot enter.
351
403
  */
352
404
  declare const clientIdentity: z.ZodObject<{
353
405
  kind: z.ZodEnum<{
354
- server_key: "server_key";
355
406
  developer: "developer";
356
- end_user: "end_user";
407
+ server_key: "server_key";
408
+ app_user: "app_user";
357
409
  }>;
358
410
  developer_id: z.ZodNullable<z.ZodUUID>;
359
- end_user_id: z.ZodNullable<z.ZodUUID>;
411
+ app_user_id: z.ZodNullable<z.ZodUUID>;
360
412
  server_key_id: z.ZodNullable<z.ZodUUID>;
361
413
  app_id: z.ZodNullable<z.ZodUUID>;
362
414
  role_id: z.ZodNullable<z.ZodUUID>;
363
415
  email: z.ZodNullable<z.ZodEmail>;
364
- act: z.ZodOptional<z.ZodObject<{
365
- admin_user_id: z.ZodUUID;
366
- }, z.core.$strict>>;
367
416
  }, z.core.$strip>;
368
417
  type ClientIdentity = z.infer<typeof clientIdentity>;
369
418
 
419
+ // SPDX-License-Identifier: Apache-2.0
420
+
370
421
  declare const asset: z.ZodObject<{
371
422
  id: z.ZodUUID;
372
423
  robot_id: z.ZodUUID;
@@ -389,23 +440,18 @@ type Asset = z.infer<typeof asset>;
389
440
  *
390
441
  * `missing` carries **the reference, verbatim, that no asset answers** — for
391
442
  * a `package://` mesh the URI the bridge could not resolve in the workspace,
392
- * and since W7's security fix also the absolute paths and bare relative paths
393
- * a URDF may carry, which the extractor sees and the sync deliberately never
394
- * offers. The sentence used to say "the `package://` URIs" and the field
395
- * carried three kinds of string (Momus-W7); it is widened here rather than
396
- * narrowed, because a developer whose URDF names `/opt/meshes/arm.stl` is
397
- * entitled to be told that nothing will ever fetch it.
443
+ * and also the absolute paths and bare relative paths a URDF may carry, which
444
+ * the extractor sees and the sync deliberately never offers. A developer whose
445
+ * URDF names `/opt/meshes/arm.stl` is entitled to be told that nothing will
446
+ * ever fetch it.
398
447
  *
399
- * The spec's example is "2 Meshes fehlen" and that number alone is a dead
400
- * end: it tells a developer to go looking through a workspace by hand. The
401
- * references are what they can act on, so the references travel.
448
+ * A bare count of what is missing is a dead end: it tells a developer to go
449
+ * looking through a workspace by hand. The references are what they can act
450
+ * on, so the references travel.
402
451
  *
403
- * **Every entry must be actionable, and that is a constraint on the
404
- * producers, not on this field (W7a).** An entry a developer cannot make
405
- * disappear by fixing what it names is a defect in whoever put it there: for
406
- * a whole wave `<texture>` references were listed here and no sync would ever
407
- * offer them, so the honest instruction behind the list was "fix this, it
408
- * will not help".
452
+ * **Every entry must be actionable, and that is a constraint on the producers,
453
+ * not on this field.** An entry a developer cannot make disappear by fixing
454
+ * what it names is a defect in whoever put it there.
409
455
  */
410
456
  declare const urdfCompleteness: z.ZodObject<{
411
457
  present: z.ZodBoolean;
@@ -423,9 +469,9 @@ declare const assetSyncStatus: z.ZodObject<{
423
469
  sync_id: z.ZodUUID;
424
470
  robot_id: z.ZodUUID;
425
471
  state: z.ZodEnum<{
472
+ failed: "failed";
426
473
  running: "running";
427
474
  succeeded: "succeeded";
428
- failed: "failed";
429
475
  }>;
430
476
  done: z.ZodNumber;
431
477
  total: z.ZodNumber;
@@ -467,9 +513,9 @@ declare const assetListResponse: z.ZodObject<{
467
513
  sync_id: z.ZodUUID;
468
514
  robot_id: z.ZodUUID;
469
515
  state: z.ZodEnum<{
516
+ failed: "failed";
470
517
  running: "running";
471
518
  succeeded: "succeeded";
472
- failed: "failed";
473
519
  }>;
474
520
  done: z.ZodNumber;
475
521
  total: z.ZodNumber;
@@ -505,8 +551,10 @@ declare const assetListResponse: z.ZodObject<{
505
551
  }, z.core.$strip>;
506
552
  type AssetListResponse = z.infer<typeof assetListResponse>;
507
553
 
554
+ // SPDX-License-Identifier: Apache-2.0
555
+
508
556
  /**
509
- * One violated §4.4 rule. `details` on the envelope stays `unknown` — codes
557
+ * One violated parameter rule. `details` on the envelope stays `unknown` — codes
510
558
  * are an open set, so their payloads cannot all be enumerated — but the
511
559
  * payload of `parameter_invalid` **is** pinned here, because otherwise every
512
560
  * consumer guesses: the cloud emits one shape, the SDK sniffs for two, the
@@ -539,83 +587,13 @@ declare const parameterInvalidDetails: z.ZodObject<{
539
587
  }, z.core.$strip>;
540
588
  type ParameterInvalidDetails = z.infer<typeof parameterInvalidDetails>;
541
589
  /**
542
- * The codes in use as of W2. The wire deliberately allows any string — this
590
+ * The codes in use today. The wire deliberately allows any string — this
543
591
  * list is the shared vocabulary, not a closed set, so a new refusal never
544
592
  * needs a contracts release before it can be reported honestly.
545
593
  */
546
- declare const ERROR_CODES: readonly ["not_found", "validation_error", "bad_request", "unknown_datapoint", "invalid_token", "protocol_mismatch", "invalid_frame", "duplicate_slug", "reserved_slug", "unknown_slug", "unknown_field_path", "unknown_type", "unknown_topic", "invalid_rate", "invalid_range", "config_conflict", "no_data", "robot_offline", "bridge_timeout", "unauthorized", "forbidden", "invalid_credentials", "token_expired", "token_revoked", "invite_expired", "invite_used", "email_taken", "identifier_taken", "weak_password", "account_blocked", "busy", "parameter_invalid", "job_lost", "publisher_busy", "unknown_command", "not_subscribable", "camera_offline", "no_snapshot_yet", "live_unavailable", "wrong_kind", "not_recorded", "not_aggregatable", "quota_exceeded", "credential_in_use", "goal_timeout", "robot_in_use", "robot_deletion_partial", "job_queue_full", "invalid_uuid", "rate_limited", "tier_required", "token_spent", "service_timeout", "asset_missing", "asset_too_large", "dynamic_registration_disabled", "client_limit_reached", "identity_conflict", "identity_not_provisioned", "idp_unavailable", "mcp_disabled", "tool_not_available", "capability_required", "last_owner", "group_not_deletable", "group_in_use", "target_state_conflict", "mcp_access_denied", "signup_closed", "draft_not_a_document", "internal_error", "not_cancellable", "unsupported_media_type", "wrong_browser", "invalid_yaml", "unstorable_yaml"];
594
+ declare const ERROR_CODES: readonly ["not_found", "validation_error", "bad_request", "unknown_datapoint", "invalid_token", "protocol_mismatch", "invalid_frame", "duplicate_slug", "reserved_slug", "unknown_slug", "unknown_field_path", "unknown_type", "unknown_topic", "invalid_rate", "invalid_range", "config_conflict", "no_data", "robot_offline", "bridge_timeout", "unauthorized", "forbidden", "invalid_credentials", "token_expired", "token_revoked", "email_taken", "identifier_taken", "weak_password", "account_blocked", "busy", "parameter_invalid", "job_lost", "publisher_busy", "unknown_command", "not_subscribable", "camera_offline", "no_snapshot_yet", "live_unavailable", "wrong_kind", "not_recorded", "not_aggregatable", "quota_exceeded", "credential_in_use", "goal_timeout", "robot_in_use", "robot_deletion_partial", "job_queue_full", "invalid_uuid", "rate_limited", "tier_required", "token_spent", "service_timeout", "asset_missing", "asset_too_large", "dynamic_registration_disabled", "client_limit_reached", "idp_unavailable", "mcp_disabled", "tool_not_available", "capability_required", "last_owner", "target_state_conflict", "signup_closed", "draft_not_a_document", "internal_error", "not_cancellable", "unsupported_media_type", "wrong_browser", "invalid_yaml", "unstorable_yaml", "registration_closed", "domain_not_allowed", "email_unverified", "origin_not_allowed", "template_invalid", "provider_disabled", "provider_misconfigured", "invalid_redirect_uri", "interaction_expired"];
547
595
  type ErrorCode = (typeof ERROR_CODES)[number];
548
596
 
549
- /**
550
- * **What an end user sees about their own consents, and why it is not
551
- * `consentGrant` (W9c, DEF-099).**
552
- *
553
- * `consentGrant` is the *record* — four ids and a scope string. A person
554
- * deciding whether to revoke something needs to recognise it, and an id is
555
- * not recognisable. So this carries the names that were on the screen when
556
- * they consented: the client's, the app's, and the role's.
557
- *
558
- * `client_id` stays, because it is what a revocation addresses — the names
559
- * are for reading, the id is for acting.
560
- */
561
- declare const consentGrantSummary: z.ZodObject<{
562
- client_id: z.ZodString;
563
- client_name: z.ZodString;
564
- app_id: z.ZodUUID;
565
- app_name: z.ZodString;
566
- role_id: z.ZodUUID;
567
- role_name: z.ZodString;
568
- scope: z.ZodString;
569
- granted_at: z.ZodISODateTime;
570
- }, z.core.$strip>;
571
- type ConsentGrantSummary = z.infer<typeof consentGrantSummary>;
572
- /**
573
- * **Revoking one grant must end the access it authorised, not merely forget
574
- * that it happened (W9c, DEF-099).**
575
- *
576
- * The register row is about a user who wants a specific client to stop, and
577
- * the failure mode to avoid is a revocation that deletes the consent row
578
- * while every already-issued token keeps working until it expires. So the
579
- * response says what was actually ended, and a caller can tell *nothing
580
- * matched* from *matched and ended*.
581
- *
582
- * `tokens_revoked` is the count of refresh **families** ended. It is not a
583
- * count of access tokens, and deliberately so: an access token is stateless
584
- * and short-lived, and a number that claimed to have revoked one would be the
585
- * kind of sentence this project keeps having to take back.
586
- *
587
- * **What actually happens to the access token is stronger than this comment
588
- * first claimed, and narrower than its correction (W9c, 2026-08-19).** The
589
- * first version said *"let the access token expire"*. It does not. The
590
- * correction then said *"the same access token dies"*, which is true and
591
- * under-specified — Data-W9c read `resolveAnyToken` instead of copying the
592
- * sentence and found what the check is actually bound to:
593
- *
594
- * `if (claims.client_id) { ...consent-grant lookup... }`
595
- *
596
- * So the immediate death is scoped to **`client_id`, not to a session and not
597
- * to the end user.** Every currently-valid token issued through *this
598
- * client's* OAuth flow for this end user dies at once — including a second
599
- * tab holding a different token from the same client. A plain `auth.login()`
600
- * session is untouched, because it carries no `client_id` for the check to
601
- * read, and a token bound to a *different* client is untouched too.
602
- *
603
- * That distinction is the whole point of revoking one grant rather than
604
- * logging somebody out: *"without touching any other client's access"* is
605
- * what this route promises, and the check is what makes it true.
606
- *
607
- * That is a better outcome than the contract promised, and it is written down
608
- * here for one reason: **a caller must not build on the weaker sentence.** If
609
- * this platform ever moves to stateless verification without the revocation
610
- * re-check, the access token would start living out its TTL again, and
611
- * anything that quietly relied on immediate death would break silently.
612
- */
613
- declare const consentRevokeResponse: z.ZodObject<{
614
- revoked: z.ZodBoolean;
615
- tokens_revoked: z.ZodNumber;
616
- }, z.core.$strip>;
617
- type ConsentRevokeResponse = z.infer<typeof consentRevokeResponse>;
618
-
619
597
  /**
620
598
  * Codes the SDK produces itself rather than relaying from the server. Kept
621
599
  * out of `@fleetless/contracts`' `ERROR_CODES` deliberately — that list is
@@ -656,7 +634,12 @@ type ConsentRevokeResponse = z.infer<typeof consentRevokeResponse>;
656
634
  * signature and is still passing an options object third; and a
657
635
  * `concurrency` on `assets.prepareUrdfScene` that is not a positive
658
636
  * integer, which would otherwise fetch nothing and return a scene that
659
- * renders blank with no error to explain why. All three are refused
637
+ * renders blank with no error to explain why. **A fourth since 3.0.0:**
638
+ * every `auth` method needing an app user's own session, called on a client
639
+ * built with a `serverKey` — `register`, `login`, `logout`, the password
640
+ * and invitation calls, both OIDC calls, the two MCP decisions and the two
641
+ * grant calls. Those threw a bare `Error` before, which a caller could only
642
+ * catch by message. All of them are refused
660
643
  * before any request is sent — as a rejection, since every one of those
661
644
  * methods is `async`. A client-side mistake to fix, not something a
662
645
  * server response could ever produce, which is why this code belongs here
@@ -675,17 +658,6 @@ type ConsentRevokeResponse = z.infer<typeof consentRevokeResponse>;
675
658
  * rather than left to surface as a confusing downstream failure from
676
659
  * `URDFLoader.parse(undefined)` or similar — the caller's fix is "sync a
677
660
  * URDF first", which this error can say directly.
678
- * - `no_hosted_login_attempt`: `auth.completeHostedLogin()` was called with
679
- * an empty `expectedState` — nothing was persisted for this attempt. A
680
- * callback landing in a different tab or window than the one that called
681
- * `beginHostedLogin`, a restored session, or storage cleared in between
682
- * all produce exactly this, and none of them is an attack. Told apart
683
- * from `state_mismatch` on purpose: the two diagnoses have different
684
- * remedies ("check how you persisted the value" versus "this response
685
- * belongs to a login you did not start"). It also closes a real gap —
686
- * comparing two *empty* strings with `!==` is `false`, so without this
687
- * check first, a caller with nothing persisted at all could reach
688
- * `state_mismatch`'s comparison having contributed no defence whatsoever.
689
661
  * - `aborted`: `assets.prepareUrdfScene()` was given an `AbortSignal` and it
690
662
  * fired — either already-aborted before the call started, or mid-flight
691
663
  * while a fetch was in progress. Normalized to this one code regardless of
@@ -698,18 +670,27 @@ type ConsentRevokeResponse = z.infer<typeof consentRevokeResponse>;
698
670
  * created (`blob:` URLs) is revoked before this throws — an aborted load
699
671
  * must not leak what it fetched before the signal fired, the same
700
672
  * guarantee a failed load already had.
701
- * - `state_mismatch`: `auth.completeHostedLogin()` was called with a `state`
702
- * that does not match the `expectedState` its own `beginHostedLogin()`
703
- * returned for this attempt (or with no `state` at all — `beginHostedLogin`
673
+ * - `state_mismatch`: `auth.completeOidcLogin()` was called with a `state`
674
+ * that does not match the `expectedState` its own `beginOidcLogin()`
675
+ * returned for this attempt — or with no `state` at all (`beginOidcLogin`
704
676
  * always sets one, so a callback carrying none does not look like a reply
705
- * to a flow this client started). Thrown before `/oauth/token` is ever
706
- * called: RFC 6749 section 10.12's whole point is that a caller must not
707
- * complete an authorization response it did not itself request, so this
708
- * check happens client-side, first, rather than being left to the server
709
- * to catch — by which point a code exchange would already have been
710
- * attempted for a flow this client never started.
677
+ * to a flow this client started), or with an **empty `expectedState`**,
678
+ * meaning nothing was persisted for this attempt at all. That last case is
679
+ * folded in rather than given its own code: two empty strings compare
680
+ * equal, so it has to be checked explicitly or the comparison defends
681
+ * nothing for exactly the callers most likely to hit it — but a caller
682
+ * branching on the code has the same next step either way, which is to
683
+ * start the sign-in again. Which of the two happened is in the message,
684
+ * because the *developer's* remedies do differ ("check how your app
685
+ * persisted the value" versus "this response belongs to a sign-in you did
686
+ * not start"). Thrown before `/api/client/oidc/exchange` is ever called:
687
+ * RFC 6749 section 10.12's whole point is that a caller must not complete
688
+ * an authorization response it did not itself request, so this check
689
+ * happens client-side, first, rather than being left to the server to
690
+ * catch — by which point a one-time code would already have been spent for
691
+ * a flow this client never started.
711
692
  */
712
- declare const SDK_ERROR_CODES: readonly ["no_session", "no_websocket", "unparseable_error", "command_timeout", "command_outcome_unknown", "unexpected_response", "invalid_option", "untrusted_absolute_url", "state_mismatch", "no_hosted_login_attempt", "no_urdf_synced", "aborted"];
693
+ declare const SDK_ERROR_CODES: readonly ["no_session", "no_websocket", "unparseable_error", "command_timeout", "command_outcome_unknown", "unexpected_response", "invalid_option", "untrusted_absolute_url", "state_mismatch", "no_urdf_synced", "aborted"];
713
694
  /**
714
695
  * The union of `SDK_ERROR_CODES` — the SDK's own client-side error
715
696
  * vocabulary. A `FleetlessError` whose `code` is one of these was raised by
@@ -1163,8 +1144,8 @@ interface AssetsApi {
1163
1144
  * in sync.
1164
1145
  *
1165
1146
  * **`urdf-loader` resolves `package://` itself, before any of this runs —
1166
- * a second resolution stage this method has to account for, measured in a
1167
- * real browser rather than read off the source.** `URDFLoader.parse()`'s own `resolvePath()`
1147
+ * a second resolution stage this method has to account for.**
1148
+ * `URDFLoader.parse()`'s own `resolvePath()`
1168
1149
  * rewrites `package://pkg/rel` using `this.packages` (default `''`) to
1169
1150
  * `/pkg/rel` — a root-relative URL — and *that* is what reaches
1170
1151
  * `loadMeshCb`/`ColladaLoader`/`manager.resolveURL()`, not the original
@@ -1270,389 +1251,351 @@ declare class InMemoryTokenStore implements TokenStore {
1270
1251
  }
1271
1252
 
1272
1253
  /**
1273
- * `beginHostedLogin()`'s input: the OAuth client the developer registered
1274
- * for this app, where the browser should come back to, and optionally what
1275
- * the resulting token should be usable against.
1254
+ * `register()`'s input. The app identifier is **not** here: the client already
1255
+ * holds one (`createClient({ appIdentifier })`) and sends it itself, so there
1256
+ * is no way for a caller to register somebody into a different app than the
1257
+ * one this client speaks for.
1276
1258
  */
1277
- interface BeginHostedLoginOptions {
1259
+ interface RegisterOptions {
1260
+ /** The address the verification mail goes to. Nothing works until that link is spent. */
1261
+ email: string;
1262
+ /** At least 12 characters — `clientRegisterRequest` refuses less with a `validation_error`. */
1263
+ password: string;
1278
1264
  /**
1279
- * The opaque `client_id` issued when the developer registered this app's
1280
- * OAuth client (console, App Settings). **Never `appIdentifier`** — they
1281
- * are deliberately different identifiers — the contracts call this "the
1282
- * `client_id` on the wire: opaque, and not the app identifier".
1265
+ * What the app should call this person. Optional, and **omitted from the
1266
+ * request entirely** when you do not pass it — `clientRegisterRequest` is a
1267
+ * strict schema, so a key carrying `undefined` would be a `422` rather than a
1268
+ * default.
1283
1269
  */
1284
- clientId: string;
1270
+ displayName?: string;
1271
+ }
1272
+ /** `acceptInvitation()`'s input — the token out of the mailed link, plus the password the account gets. */
1273
+ interface AcceptInvitationOptions {
1274
+ /** The `token` from the invitation link the developer's app was linked to. */
1275
+ token: string;
1276
+ /** At least 12 characters. The invitation fixes the role; this call fixes the credential. */
1277
+ password: string;
1278
+ /** Optional, and omitted from the request entirely when absent — same strict-schema reason as `RegisterOptions.displayName`. */
1279
+ displayName?: string;
1280
+ }
1281
+ /** One sign-in button on the app's own login screen, as `listProviders()` lists it. */
1282
+ interface ProviderButton {
1283
+ /** What `beginOidcLogin` addresses this provider by. */
1284
+ slug: string;
1285
+ /** The label the developer configured, to be rendered on the button. */
1286
+ name: string;
1287
+ }
1288
+ /** `beginOidcLogin()`'s input: which provider, and where Fleetless should send the browser back to. */
1289
+ interface BeginOidcLoginOptions {
1290
+ /** A `slug` from `listProviders()`. An unknown one is a `404` when the browser reaches the start route, not here. */
1291
+ slug: string;
1285
1292
  /**
1286
- * Must be registered, byte-for-byte, as one of that client's
1287
- * `redirect_uris` — matching at the server is exact-string, never a
1288
- * prefix (the contracts' `redirectUri` doc comment says why).
1293
+ * Where the browser comes back to with `?code=…&state=…` (or `?error=…`).
1294
+ * Checked against the app's **allowed origins** server-side, by origin — so
1295
+ * the path is yours to choose and the origin is not.
1296
+ *
1297
+ * **Two shapes are refused outright, before the origin is compared**, and
1298
+ * neither refusal mentions them: a URL carrying a **fragment**
1299
+ * (`https://app.example.com/#/auth/callback`) and one carrying **userinfo**
1300
+ * (`https://someone@app.example.com/cb`). Both come back as a flat
1301
+ * `400 invalid_redirect_uri` reading "The redirect_uri is not an origin this
1302
+ * app answers for", which sends people to re-check an allow-list that was
1303
+ * never the problem.
1304
+ *
1305
+ * The fragment case is the one that costs time, because hash routing is the
1306
+ * default for a static-hosted SPA with no server rewrite. Give the callback
1307
+ * a real path (`/auth/callback`) and let your router pick the hash route up
1308
+ * from there; a fragment is a browser-side construct the redirect could not
1309
+ * carry a code in anyway.
1289
1310
  */
1290
1311
  redirectUri: string;
1312
+ }
1313
+ /** What `beginOidcLogin()` returns. Nothing here has touched the network. */
1314
+ interface OidcLoginRequest {
1291
1315
  /**
1292
- * The OAuth scopes to request, space-separated. Omit it to get the
1293
- * client's registered default, which is what an ordinary app login wants.
1294
- */
1295
- scope?: string;
1296
- /**
1297
- * RFC 8707 audience binding: the resource this
1298
- * session's token should be usable against. **Omit it for an ordinary app
1299
- * login.** A token with no `resource` carries no `aud` and works
1300
- * unrestricted against this app's own REST surface exactly as it always
1301
- * has; that path is unaffected by this field's existence. Only a caller
1302
- * that is itself going to present the token to an audience-checking
1303
- * resource needs to ask for one.
1304
- *
1305
- * **`/oauth/authorize` accepts exactly two shapes** — anything else is
1306
- * refused with `invalid_target` on the redirect back, before a code is
1307
- * ever issued (the cloud's own known-resource check):
1308
- *
1309
- * - `<base>/mcp-stub/resource` — the global OAuth resource stub, matched
1310
- * byte-for-byte.
1311
- * - `<base>/mcp-stub/resource/<app_identifier>` — the per-app stub, and
1312
- * only for **the calling client's own app**. This is an existence check
1313
- * plus an ownership check, not a shape check: the app must exist *and*
1314
- * be the one this `clientId` is registered to, so a client on app A can
1315
- * never be minted a token whose `aud` names app B.
1316
- *
1317
- * Both are served and validated by the platform's resource stub, whose
1318
- * validator refuses a token with **no** `aud` exactly as hard as one with
1319
- * the wrong `aud` —
1320
- * "unscoped" must never read as "for me" — comparing by equality, never
1321
- * by prefix.
1322
- *
1323
- * **`<base>/mcp/<app_identifier>` is not a resource any more.** There is
1324
- * no per-app MCP endpoint: the cloud registers one central, non-parametric
1325
- * `POST /mcp` (the contracts' `MCP_ENDPOINT_PATH`), and the
1326
- * `/oauth/authorize` branch that used to
1327
- * accept a `/mcp/<app>` resource was deleted along with the app-level
1328
- * `mcp_enabled` flag. Asking for one now yields `invalid_target` for
1329
- * every app, **including your own**. The central MCP endpoint has its own
1330
- * OAuth flow, which this SDK's hosted login does not drive —
1331
- * `beginHostedLogin` always targets
1332
- * `OAUTH_PATHS.authorize`.
1333
- *
1334
- * Whatever you name here is re-checked at the token exchange: it must
1335
- * match what the code was authorized for, and it must still name a
1336
- * resource this client may be issued a token for.
1316
+ * Send the end user's browser here. **This SDK does not navigate** — it has
1317
+ * no opinion about whether that is a full page load, a popup or a native web
1318
+ * view, the same boundary `cameras.live` draws by handing back a URL and a
1319
+ * token and stopping there.
1337
1320
  */
1338
- resource?: string;
1339
- }
1340
- /** What `beginHostedLogin()` returns — nothing here has touched the network yet. */
1341
- interface HostedLoginRequest {
1342
- /** Send the end user's browser here to start the hosted login page. */
1343
1321
  url: string;
1344
1322
  /**
1345
- * Persist this alongside `codeVerifier` before navigating away, and pass
1346
- * both back into `completeHostedLogin`. **This SDK does not persist them
1347
- * for you.** The redirect back to `redirectUri` is a fresh page load for a
1348
- * browser app — nothing kept in this SDK's own memory survives it (the
1349
- * same reasoning `TokenStore` states: the SDK itself never assumes a
1350
- * browser, or any storage, exists). An in-memory default
1351
- * here would not be merely suboptimal, it would be broken for the primary
1352
- * use case while looking like it worked for anything that never actually
1353
- * navigates away. `sessionStorage`, a signed cookie, or a plain variable
1354
- * (a popup flow that never truly navigates) are all valid — that choice is
1355
- * the caller's.
1323
+ * Persist this next to `codeVerifier` **before navigating away**, and pass
1324
+ * both back into `completeOidcLogin`. The SDK does not persist them for you:
1325
+ * the redirect back is a fresh page load for a browser app, and nothing kept
1326
+ * in this SDK's memory survives it. `sessionStorage`, a signed cookie or a
1327
+ * plain variable (a popup flow that never truly navigates) are all valid —
1328
+ * that choice is the caller's, and an in-memory default here would look like
1329
+ * it worked right up until the first real redirect.
1356
1330
  */
1357
1331
  state: string;
1358
1332
  /**
1359
- * The PKCE code verifier for this attempt. Persist it exactly as
1360
- * `state` above and pass it back to `completeHostedLogin` — it is what
1361
- * proves the code exchange comes from the client that started the flow.
1333
+ * The PKCE code verifier for this attempt — persist it exactly as `state`.
1334
+ * **The app runs its own PKCE against Fleetless**, a second exchange
1335
+ * independent of the one Fleetless runs against the identity provider, which
1336
+ * is what makes the one-time code in the redirect worth nothing to whoever
1337
+ * else reads that URL.
1362
1338
  */
1363
1339
  codeVerifier: string;
1364
1340
  }
1365
- /** `completeHostedLogin()`'s input — the redirect back, plus what `beginHostedLogin` returned for this same attempt. */
1366
- interface CompleteHostedLoginOptions {
1367
- /** The `code` query parameter from the redirect back to `redirectUri`. */
1341
+ /** `completeOidcLogin()`'s input — the redirect back, plus what `beginOidcLogin` returned for this same attempt. */
1342
+ interface CompleteOidcLoginOptions {
1343
+ /** The `code` query parameter from the redirect back to `redirectUri`. It lives 60 seconds. */
1368
1344
  code: string;
1369
1345
  /** The `state` query parameter from that same redirect. */
1370
1346
  state: string;
1371
- /**
1372
- * The `state` this attempt's `beginHostedLogin` returned. Checked against
1373
- * `state` above **before any network call** — the whole point of RFC
1374
- * 6749 section 10.12 is that a caller must not complete an authorization
1375
- * response it did not itself request.
1376
- */
1347
+ /** The `state` this attempt's `beginOidcLogin` returned. Compared **before any network call**. */
1377
1348
  expectedState: string;
1378
- /** The `codeVerifier` this attempt's `beginHostedLogin` returned. */
1349
+ /** The `codeVerifier` this attempt's `beginOidcLogin` returned. */
1379
1350
  codeVerifier: string;
1380
- /** Must be the exact same `client_id` passed to `beginHostedLogin`. */
1381
- clientId: string;
1382
- /** Must be the exact same string passed to `beginHostedLogin`. */
1383
- redirectUri: string;
1384
- /**
1385
- * Must be the exact same string passed to `beginHostedLogin`, if any.
1386
- * Resending it here is not what binds the audience — the server already
1387
- * bound `resource` to the authorization code at `/oauth/authorize` and
1388
- * mints `aud` from that stored value regardless of what this call sends —
1389
- * but RFC 8707 section 2 expects a client to name the resource at both steps,
1390
- * and the cloud rejects a *mismatched* resend outright (`invalid_target`).
1391
- * Omit it here exactly when it was omitted at `beginHostedLogin`.
1392
- */
1393
- resource?: string;
1394
1351
  }
1395
1352
  /**
1396
- * What `logout()` resolves with — five separable facts, not one nullable
1397
- * URL, mirroring the wire's `clientLogoutResponse`.
1398
- *
1399
- * **`idp_logout` is `null` whenever the server never answered**, which
1400
- * happens for two different reasons: there was no local session to ask
1401
- * about, or the request itself failed. Either way there is nothing to
1402
- * report — not even "not federated", because this client never learned that
1403
- * either.
1404
- *
1405
- * `revoked` does not identify which of the two you got. It is `false` only
1406
- * for the failed request; **a logout with no local session at all resolves
1407
- * `{ revoked: true, idp_logout: null }`**, since nothing lingers
1408
- * server-side and that counts as revoked. So `revoked: false` does imply
1409
- * `idp_logout: null`, and the converse does not hold. Do not read the two
1410
- * fields as one bit.
1411
- *
1412
- * When a real response did come back, `revoked` is `true` and `idp_logout`
1413
- * is one of:
1353
+ * What `approveMcpInteraction`/`denyMcpInteraction` resolve with: **where to
1354
+ * send the browser**, and nothing else.
1414
1355
  *
1415
- * - `{ status: 'redirect', url }` — send the browser here to end the
1416
- * session at the IdP too. Nothing else in this SDK does that navigation
1417
- * for you (same "this SDK is thin" reasoning as `cameras.live`).
1418
- * - `{ status: 'not_federated' }` — this session never came from an IdP;
1419
- * there is nothing else to end.
1420
- * - `{ status: 'unsupported_by_idp' }` — it did, and the IdP publishes no
1421
- * `end_session_endpoint` (RP-initiated logout is optional in OIDC).
1422
- * - `{ status: 'hint_unavailable' }` — it did, the IdP *can* end the
1423
- * session, and Fleetless has nothing to ask it with: the stored
1424
- * `id_token_hint` could not be decrypted, or the session predates the fix
1425
- * that started keeping one.
1426
- * - `{ status: 'session_unknown' }` — the server did not find this session at
1427
- * all (the token was unknown, already superseded, revoked, or expired), so
1428
- * it can say **nothing** about an IdP.
1356
+ * A denial carries a redirect too, with `error=access_denied` on it — a client
1357
+ * that is refused has to learn so from its own callback rather than from a page
1358
+ * nobody sent it, so both outcomes end the same way for the app: navigate here.
1359
+ */
1360
+ interface McpInteractionDecision {
1361
+ /** The absolute URL to navigate to. Wire field `redirect_to`. */
1362
+ redirectTo: string;
1363
+ }
1364
+ /**
1365
+ * Who the caller is, reachable as `client.auth` — the whole client
1366
+ * authentication API, as JSON.
1429
1367
  *
1430
- * **`unsupported_by_idp` and `hint_unavailable` both mean the IdP session
1431
- * survives and this platform cannot end it** — do not render either one the
1432
- * same as `not_federated`; that reports a session as fully ended when it
1433
- * isn't.
1368
+ * **Fleetless serves an app user no page.** The developer's own UI owns every
1369
+ * screen: login, registration, verification, invitation acceptance, password
1370
+ * reset, the provider buttons and the MCP consent. These methods are what those
1371
+ * screens call. The hosted, app-branded login and consent pages this SDK used
1372
+ * to drive are gone, along with `beginHostedLogin`/`completeHostedLogin`.
1434
1373
  *
1435
- * **`session_unknown` is a different kind of nothing, and the reason it
1436
- * exists is a second logout.** A double click, a repeated POST, an
1437
- * app that logs out on unmount *and* on a route change: the last call wins,
1438
- * and before this outcome existed it answered `not_federated` — a positive
1439
- * claim about an IdP the server had never looked up. An app that treats it as
1440
- * *"fully logged out"* skips a redirect the **first** call may well have
1441
- * returned, and the user stays signed in at the IdP after clicking log out.
1374
+ * **The enumeration discipline is the design's, and it shapes this surface.**
1375
+ * `register`, `resendVerification` and `requestPasswordReset` resolve for every
1376
+ * policy-allowed request whether or not the address exists, and `login` answers
1377
+ * the identical `invalid_credentials` for a wrong password, a blocked account
1378
+ * and an unverified one. So: *resolving does not mean an account exists*, and
1379
+ * the only honest refusals are the ones about policy rather than about a
1380
+ * person — `registration_closed`, `domain_not_allowed`, `quota_exceeded`.
1442
1381
  *
1443
- * Note that `revoked` cannot help you here either: it reports whether the
1444
- * HTTP call succeeded, not whether a session was found — so this case
1445
- * arrives as `{ revoked: true, idp_logout: { status: 'session_unknown' } }`,
1446
- * never as `null`.
1382
+ * **A client built with a `serverKey` refuses everything that needs an app
1383
+ * user's own session** with `invalid_option`, before any request. `me()`,
1384
+ * `listProviders()`, `mcpInteraction()` and `oidcErrorFromCallback()` still
1385
+ * work on one: the first is what a server key is *for*, the next two are public
1386
+ * reads the cloud answers without any credential at all, and the last touches
1387
+ * no network.
1447
1388
  */
1448
- interface LogoutResult {
1449
- /**
1450
- * Whether the server-side revoke actually happened. It reports the fate
1451
- * of the HTTP call, not whether a session was found — `false` means the
1452
- * refresh family may still be alive even though this client has
1453
- * forgotten it.
1454
- */
1455
- revoked: boolean;
1389
+ interface AuthApi {
1456
1390
  /**
1457
- * What the server could say about the identity provider behind this
1458
- * session, or `null` when there was no answer to report at all — either
1459
- * because there was no local session to ask about, or because the request
1460
- * failed. `null` is **not** the same as `not_federated`, which is a real
1461
- * finding about a real session.
1462
- *
1463
- * When it is not `null` it is one of five statuses: `redirect` (with a
1464
- * `url` to send the browser to, to end the session at the identity
1465
- * provider too), `not_federated` (this session never came from one),
1466
- * `unsupported_by_idp` (it did, and the provider offers no
1467
- * RP-initiated logout), `hint_unavailable` (it did, the provider can end
1468
- * the session, and Fleetless has nothing to ask it with), or
1469
- * `session_unknown` (the server did not find this session, so it can say
1470
- * nothing about a provider).
1471
- *
1472
- * **`unsupported_by_idp` and `hint_unavailable` both mean the identity
1473
- * provider's session survives and this platform cannot end it.** Render
1474
- * either one like `not_federated` and you report a session as fully ended
1475
- * when it is not.
1391
+ * Self-registration. Writes the account as `pending_verification` and mails
1392
+ * the app's verification link; **the account cannot log in until that link is
1393
+ * spent** (`verifyEmail`).
1394
+ *
1395
+ * Resolves on the route's `202` — which the cloud answers for every
1396
+ * policy-allowed request, whether the address was new or already known. It is
1397
+ * not a claim that an account was created, and an app that renders it as one
1398
+ * ("welcome, Ada!") is showing a stranger the enumeration oracle this whole
1399
+ * family is built to avoid. Render "check your mail" instead.
1400
+ *
1401
+ * Throws a `FleetlessError` carrying the cloud's own code for a refusal, and
1402
+ * that is the distinction this method exists to preserve: `registration_closed`
1403
+ * (the app has self-registration off), `domain_not_allowed` (the address is
1404
+ * outside the app's allowed domains), `quota_exceeded` (the org has as many
1405
+ * app users as its quota allows) and `not_found` (no app carries this
1406
+ * client's `appIdentifier`) are all things the app can say out loud, because
1407
+ * none of them is about whether a person exists.
1476
1408
  */
1477
- idp_logout: ClientLogoutResponse['idp_logout'] | null;
1478
- }
1479
- /**
1480
- * Who the caller is, reachable as `client.auth`. A client built with a
1481
- * `tokenStore` uses the full surface. One built with a `serverKey` already
1482
- * has an identity and no user session, so **`me()` is the only method it
1483
- * can call** — `login`, `beginHostedLogin`, `completeHostedLogin`,
1484
- * `logout`, `changePassword` and `passwordResetUrl` all throw on one.
1485
- */
1486
- interface AuthApi {
1487
- /** Exchanges email + password, and the client's configured app identifier, for a session. */
1488
- login(email: string, password: string): Promise<void>;
1409
+ register(input: RegisterOptions): Promise<void>;
1489
1410
  /**
1490
- * Starts the hosted login flow: a Fleetless-served
1491
- * login page an app's end user is redirected to, with optional
1492
- * per-app IdP federation. Builds the `/oauth/authorize` URL (Authorization
1493
- * Code + PKCE, S256 only — OAuth 2.1 removes `plain`) and generates the
1494
- * `state`/`codeVerifier` PKCE and CSRF protection need. **Makes no network
1495
- * call** — everything here is local, so nothing about the app, the
1496
- * client, or the redirect URI is validated until the browser actually
1497
- * reaches `/oauth/authorize`.
1498
- *
1499
- * Async only because computing the S256 `code_challenge` needs
1500
- * `crypto.subtle.digest`, which the Web Crypto API only ever offers as a
1501
- * promise — there is no synchronous digest to call instead.
1411
+ * Spends a verification token and **stores the session it answers with**, so
1412
+ * the person is not asked to log in immediately after proving they can read
1413
+ * the mail.
1414
+ *
1415
+ * `token_spent` covers unknown, expired and already-used alike — one code,
1416
+ * because the remedy is one thing: ask for a fresh link with
1417
+ * `resendVerification`. An app rendering this refusal should offer that.
1502
1418
  */
1503
- beginHostedLogin(options: BeginHostedLoginOptions): Promise<HostedLoginRequest>;
1419
+ verifyEmail(token: string): Promise<void>;
1420
+ /** Asks for the verification mail again. Resolves on `202` for every policy-allowed request, existing address or not — same reason as `register`. */
1421
+ resendVerification(email: string): Promise<void>;
1504
1422
  /**
1505
- * Completes the hosted login flow: checks `state` against `expectedState`
1506
- * (before any network call — see `CompleteHostedLoginOptions.expectedState`),
1507
- * exchanges `code` for tokens at `/oauth/token`, and stores them via the
1508
- * same `tokenStore` `login()` uses.
1509
- *
1510
- * **Two distinct refusals before that check, not one.** An empty
1511
- * `expectedState` throws `no_hosted_login_attempt` — nothing was
1512
- * persisted for this attempt (a different tab, a restored session,
1513
- * cleared storage), not necessarily an attack. Only once `expectedState`
1514
- * is actually present does a mismatch (or a missing `state` on the
1515
- * callback itself) throw `state_mismatch`. The two are told apart on
1516
- * purpose: they call for different remedies, and collapsing them would
1517
- * tell a developer debugging an ordinary storage gap that their app is
1518
- * under attack.
1519
- *
1520
- * Once past that check and the exchange completes, `me()`, `logout()`,
1521
- * `changePassword()` and silent refresh all behave identically afterwards,
1522
- * regardless of which flow the session started from. Both routes into a
1523
- * session end at the same Fleetless token: not merely that the bytes
1524
- * match, but that every existing code path treats the result the same
1525
- * way.
1526
- *
1527
- * The wire response is the envelope of RFC 6749 section 5.1 (`token_type`, optional
1528
- * `scope`), not `sessionTokens` — this method normalizes one into the
1529
- * other before storing. **Refresh needs no separate handling**: the cloud
1530
- * mints these tokens through the same session mechanism `/api/client/login`
1531
- * uses (same `refresh_tokens` row, same rotation), so the existing silent
1532
- * refresh (`/api/client/refresh`) already works for a hosted-login
1533
- * session — nothing about the origin of a session is tracked or needs to
1534
- * be.
1535
- *
1536
- * Throws with the OAuth error code as `.code` (e.g. `invalid_grant` for an
1537
- * expired or already-used `code`) if the exchange itself fails — a
1538
- * different vocabulary from every other method on this interface, because
1539
- * `/oauth/token` answers in the shape of RFC 6749 section 5.2, not `apiError`.
1540
- *
1541
- * **Makes exactly one request to `/oauth/token` — never retried, no
1542
- * timeout-and-resend, no internal concurrency of its own.** Stated
1543
- * because the constraint that matters here is not this method's, it is
1544
- * the caller's: **never call this a second time for the same `code`
1545
- * while a first call is still in flight** (a plain "the first attempt
1546
- * looked like it timed out, so retry" is exactly the shape this warns
1547
- * against — it is not a defect in this method, since this method itself
1548
- * has nothing that could ever cause that). The platform treats a second
1549
- * presentation of an authorization code as theft and revokes the whole
1550
- * token family it belongs to, deliberately, even though a plain
1551
- * double-submission looks identical on the wire —
1552
- * because the blast radius is bounded (only a caller already holding the
1553
- * correct `code_verifier` and `client_id` can trigger it, so a merely
1554
- * *sniffed* code cannot lock anyone out) and the alternative is a
1555
- * narrower defence against a real theft.
1556
- *
1557
- * **What actually happens if two requests race, measured:**
1558
- * one of the two receives `200` with a refresh token that the server has
1559
- * already revoked. The access token in that same response keeps working
1560
- * normally for the rest of its short TTL — nothing about the race is
1561
- * visible yet. The failure surfaces at this session's **first silent
1562
- * refresh**, as `token_revoked`, potentially many minutes after the race
1563
- * that actually caused it and with nothing in that later error pointing
1564
- * back to a retry that "worked". If your own framework, an HTTP client
1565
- * wrapper, or a user's impatient double-click can cause this method to
1566
- * be invoked twice concurrently for the same redirect, guard against
1567
- * that at the call site — a simple in-flight flag or disabling the
1568
- * triggering control is enough, since there is only ever one legitimate
1569
- * exchange per authorization code.
1423
+ * Exchanges email + password, and the client's configured app identifier, for
1424
+ * a session.
1425
+ *
1426
+ * `invalid_credentials` is answered identically for a wrong password, a
1427
+ * blocked account and one still waiting to verify. Do not try to tell them
1428
+ * apart — there is nothing in the answer that does, deliberately.
1570
1429
  */
1571
- completeHostedLogin(options: CompleteHostedLoginOptions): Promise<void>;
1430
+ login(email: string, password: string): Promise<void>;
1572
1431
  /**
1573
- * Ends the session: revokes the whole refresh-token family server-side
1574
- * (a stolen refresh token stops working immediately) and closes this
1575
- * client's live realtime connection, if it has one. Then clears the
1576
- * local store. Never rejects and always clears the store, even if the
1577
- * server call fails: a user who presses "log out" must end up logged out
1578
- * locally regardless of the network. `revoked` reports whether the
1579
- * server-side revoke actually happened — `false` means the refresh
1580
- * family may still be alive server-side even though this client has
1581
- * forgotten it; an app that cares (a kiosk, a shared workstation) can
1582
- * warn the user or retry, one that doesn't can ignore it.
1432
+ * Ends the session: revokes the whole refresh-token family server-side (a
1433
+ * stolen refresh token stops working immediately), closes this client's live
1434
+ * realtime connection if it has one, and clears the local store.
1435
+ *
1436
+ * **Never rejects, and always clears the store**, even if the server call
1437
+ * fails: a user who presses "log out" must end up logged out locally
1438
+ * regardless of the network.
1583
1439
  *
1584
1440
  * **What this does not do:** invalidate the access token already issued.
1585
- * Access-token checks are stateless (a signed JWT, verified without a
1586
- * server-side lookup) — logout has nothing to flip on that token, only
1587
- * on the refresh family behind it. A token stolen before logout keeps
1588
- * working on REST, and can still open a *new* realtime connection, until
1589
- * it expires on its own — at most 15 minutes. This is a deliberate
1590
- * boundary of the stateless-JWT design (the same one that lets a role
1591
- * change, a block, or a membership removal take effect on the very next
1592
- * request without a fresh token), not a bug — but a kiosk or shared
1593
- * workstation needs to know that number.
1594
- *
1595
- * **Nor does it end a federated session at the identity provider.**
1596
- * Ending the Fleetless session and ending the IdP session are
1597
- * two different things — see `LogoutResult.idp_logout`. This method does
1598
- * not act on that information itself (no redirect, no fetch to the IdP);
1599
- * it only reports what the server found, the same "this SDK stays thin"
1600
- * boundary as everywhere else (`cameras.live` hands back a URL and a
1601
- * token and stops there too).
1602
- *
1603
- * **`idp_logout` is `null` when there was no server answer to report** —
1604
- * and that is *not* the same as `revoked === false`.
1605
- *
1606
- * Calling `logout()` with **no local session** returns
1607
- * `{ revoked: true, idp_logout: null }`, so `null` is not the same as
1608
- * `revoked === false` and the two fields are not one bit. TypeScript
1609
- * catches a caller who forgets (the type is `| null`); a JavaScript caller
1610
- * does not.
1611
- *
1612
- * So: **`null` means this client had nothing to send or the request never
1613
- * answered.** It is not `not_federated`, which is a real finding about a
1614
- * real session — conflating the two is the ambiguity this shape exists to
1615
- * remove, and it is the reason to read both fields rather than one.
1441
+ * Access-token checks are stateless (a signed JWT, verified without a lookup),
1442
+ * so logout has nothing to flip on that token — only on the refresh family
1443
+ * behind it. A token stolen before logout keeps working on REST, and can
1444
+ * still open a *new* realtime connection, until it expires on its own, at
1445
+ * most 15 minutes. That is a deliberate boundary of the stateless-JWT design,
1446
+ * not a bug, but a kiosk or a shared workstation needs to know the number.
1447
+ *
1448
+ * **Nor does it end a session at the identity provider.** It used to report
1449
+ * what was left of one; that apparatus belonged to the hosted login, where
1450
+ * Fleetless owned the browser. The app owns it now, and an app that wants to
1451
+ * end a provider session redirects there itself — knowing its own provider,
1452
+ * which Fleetless never did better than it.
1616
1453
  */
1617
- logout(): Promise<LogoutResult>;
1618
- /** Who the caller turned out to be, without decoding a token client-side. */
1454
+ logout(): Promise<void>;
1455
+ /** Who the caller turned out to be, without decoding a token client-side — which is how apps end up trusting claims nobody verified. */
1619
1456
  me(): Promise<ClientIdentity>;
1620
1457
  /**
1621
- * Changes the current end user's password.
1622
- *
1623
- * `currentPassword` is required by the server even though the session
1624
- * already proves identity — it is what stops a stolen *session* from
1625
- * becoming a stolen *account* (see `passwordChangeRequest` in
1626
- * `@fleetless/contracts`).
1627
- *
1628
- * **Every other session of this identity is revoked on success, and this
1629
- * call's own session is re-issued, not left alone.** `passwordChangeRequest`
1630
- * carries nothing that identifies the caller's own refresh family, so the
1631
- * server cannot spare one token out of the family it just revoked — it
1632
- * revokes all of them and hands back a fresh pair, which this method
1633
- * stores exactly like `login` does. Skipping that store would leave the
1634
- * caller holding tokens the server has already revoked, working only
1635
- * until the access token expires and then silently logged out —
1636
- * indistinguishable from the change having failed, which is the one
1637
- * outcome this route exists to prevent. A user with other tabs or
1638
- * devices logged in will see *those* signed out the moment this
1639
- * resolves; if your app does not already make that consequence visible
1640
- * before they confirm, they will find out from a support ticket instead
1641
- * of from you.
1458
+ * Changes the current app user's password.
1459
+ *
1460
+ * `currentPassword` is required even though the session already proves
1461
+ * identity — it is what stops a stolen *session* from becoming a stolen
1462
+ * *account*.
1463
+ *
1464
+ * **Every other session of this identity is revoked, and this call's own
1465
+ * session is re-issued rather than spared.** The request carries nothing
1466
+ * identifying the caller's own refresh family, so the server revokes all of
1467
+ * them and hands back a fresh pair, which this method stores exactly like
1468
+ * `login`. A user with other tabs or devices signed in will see those signed
1469
+ * out the moment this resolves; if your app does not make that consequence
1470
+ * visible before they confirm, they will find out from a support ticket.
1642
1471
  */
1643
1472
  changePassword(currentPassword: string, newPassword: string): Promise<void>;
1473
+ /** Asks for a reset link. Resolves on `202` for a known and an unknown address alike — the answer says nothing about which it was. */
1474
+ requestPasswordReset(email: string): Promise<void>;
1644
1475
  /**
1645
- * The hosted password-reset page, served by the platform on the API
1646
- * origin (`/reset-password`). An app links a user there; nothing is
1647
- * called. The two former methods that posted to `/api/client/password/…`
1648
- * are gone with the routes they named (2.0.0).
1649
- *
1650
- * The page owns the whole flow — it takes the address, mails a
1651
- * single-use link, and takes the new password on the way back — so there
1652
- * is nothing for an app to sequence and nothing here that could reveal
1653
- * whether an address belongs to an account.
1476
+ * Spends a reset token, sets the new password and **stores the session it
1477
+ * answers with**. Every refresh family of that user is revoked first — a
1478
+ * forgotten password is one of the two states where somebody else may be
1479
+ * holding a session.
1654
1480
  */
1655
- passwordResetUrl(): string;
1481
+ confirmPasswordReset(token: string, newPassword: string): Promise<void>;
1482
+ /**
1483
+ * Accepts an app invitation: creates the account (or activates one invited
1484
+ * before it existed) with the role the invitation fixed, and **stores the
1485
+ * session**.
1486
+ *
1487
+ * An invitation always bypasses the app's domain whitelist — a developer
1488
+ * inviting somebody by hand has already made the decision the whitelist
1489
+ * automates.
1490
+ */
1491
+ acceptInvitation(input: AcceptInvitationOptions): Promise<void>;
1492
+ /**
1493
+ * The app's **enabled** sign-in providers, for drawing the buttons on your
1494
+ * own login screen. A disabled provider is not a button that refuses; it is a
1495
+ * button that is not there.
1496
+ *
1497
+ * Public and unauthenticated, and it carries nothing but `slug` and `name` on
1498
+ * purpose: the issuer, the client id, the scopes and the linking policy are
1499
+ * management-side facts that would tell a stranger how the app's federation
1500
+ * is configured.
1501
+ */
1502
+ listProviders(): Promise<ProviderButton[]>;
1503
+ /**
1504
+ * Builds the URL that starts a federated sign-in, with a fresh `state` and a
1505
+ * fresh PKCE verifier. **Makes no network call and does not navigate** —
1506
+ * persist `state` and `codeVerifier`, then send the browser to `url`.
1507
+ *
1508
+ * Nothing about the app, the provider or the redirect URI is validated here;
1509
+ * it is all checked when the browser actually reaches the route, in that
1510
+ * order, with the redirect target checked before the provider so that a
1511
+ * caller who got the target wrong learns nothing about which providers the
1512
+ * app has.
1513
+ *
1514
+ * Async only because the S256 `code_challenge` needs `crypto.subtle.digest`,
1515
+ * which the Web Crypto API only ever offers as a promise.
1516
+ */
1517
+ beginOidcLogin(input: BeginOidcLoginOptions): Promise<OidcLoginRequest>;
1518
+ /**
1519
+ * Completes a federated sign-in: checks `state` against `expectedState`,
1520
+ * trades the one-time `code` for a session, and stores it.
1521
+ *
1522
+ * **The state check runs before any request is sent.** RFC 6749 §10.12's
1523
+ * whole point is that a client must not complete an authorization response it
1524
+ * did not itself request — a check made after the exchange would already have
1525
+ * spent a code for a flow this client never started. Both an outright
1526
+ * mismatch and an *empty* `expectedState` throw `state_mismatch`; the message
1527
+ * says which, because the remedies differ ("check how your app persisted the
1528
+ * value" versus "this response belongs to a sign-in you did not start") even
1529
+ * though the next step is the same either way — start the sign-in again.
1530
+ *
1531
+ * The code lives 60 seconds and is single-use. Unknown, expired, replayed and
1532
+ * "the account was blocked in between" all arrive as one `token_spent`,
1533
+ * because the app has nothing different to do about any of them.
1534
+ */
1535
+ completeOidcLogin(input: CompleteOidcLoginOptions): Promise<void>;
1536
+ /**
1537
+ * Reads a **failed** federated sign-in off the redirect back, as a
1538
+ * `FleetlessError` you can branch on, or `null` when the callback carries no
1539
+ * `error` at all.
1540
+ *
1541
+ * Fleetless renders no page for these: the reason is carried to your own
1542
+ * `redirectUri` as `?error=<code>`, and this turns that string into the same
1543
+ * error type every other method throws. A code the contracts define (see
1544
+ * `ClientOidcErrorCode`) becomes that code verbatim; anything else becomes
1545
+ * `unexpected_response` with the raw value in the message, rather than being
1546
+ * passed through as a code neither side defines.
1547
+ *
1548
+ * Purely local — it parses a query string and asks nothing.
1549
+ */
1550
+ oidcErrorFromCallback(params: URLSearchParams): FleetlessError | null;
1551
+ /**
1552
+ * Reads a pending MCP authorization by the interaction id the browser
1553
+ * arrived with, so the app can render its own consent screen.
1554
+ *
1555
+ * **`client_name` is a string the client typed about itself** during an
1556
+ * unauthenticated dynamic registration — nobody checked it, which is why
1557
+ * `client_name_verified` is the literal `false` rather than a boolean with a
1558
+ * `true` branch that could never happen. Do not render it as an identity.
1559
+ *
1560
+ * `interaction_expired` means exactly that: ten minutes ran out, or the id
1561
+ * was never real. Both answer the same way, so the screen to show is "that
1562
+ * took too long, start again" rather than an error.
1563
+ *
1564
+ * **Call this with the app user already signed in.** The route needs no
1565
+ * credential, but it reads one if present, and `already_granted` is `false`
1566
+ * for an anonymous read whatever the truth is — so a consent screen rendered
1567
+ * from an unauthenticated call asks a person to agree to something they
1568
+ * agreed to already. An **expired** token counts as anonymous to this route,
1569
+ * which answers `200` rather than refusing, so this method probes the
1570
+ * session's liveness first and refreshes if it can; a session that cannot be
1571
+ * refreshed is not an error here, it is genuinely anonymous.
1572
+ */
1573
+ mcpInteraction(id: string): Promise<ClientMcpInteraction>;
1574
+ /** Approves a pending MCP authorization on behalf of the signed-in app user, and returns where to send the browser. */
1575
+ approveMcpInteraction(id: string): Promise<McpInteractionDecision>;
1576
+ /** Denies one. Also returns a redirect — with `error=access_denied` on it, so the client learns from its own callback. */
1577
+ denyMcpInteraction(id: string): Promise<McpInteractionDecision>;
1578
+ /**
1579
+ * Every MCP client this app user has standing consent for — the "connected
1580
+ * apps" list, and the door out of a decision a person could otherwise make
1581
+ * once and never unmake. A withdrawn grant is never listed.
1582
+ *
1583
+ * `client_name_verified` is `false` here for the reason it is on
1584
+ * `mcpInteraction`, and it matters more rather than less: a list like this is
1585
+ * read long after the moment of approval, when nobody remembers what they
1586
+ * clicked.
1587
+ */
1588
+ listMcpGrants(): Promise<McpConsentGrant[]>;
1589
+ /**
1590
+ * Withdraws one standing consent by the client's id.
1591
+ *
1592
+ * Resolves whether or not there was anything to withdraw — a client id this
1593
+ * account never approved and one it withdrew a minute ago both land on the
1594
+ * end state the caller asked for. A refusal there would tell a caller which
1595
+ * clients an account has connected, and would turn a double-clicked button
1596
+ * into a failure.
1597
+ */
1598
+ revokeMcpGrant(clientId: string): Promise<void>;
1656
1599
  }
1657
1600
 
1658
1601
  /**
@@ -1937,58 +1880,6 @@ interface DatapointsApi {
1937
1880
  }): Promise<HistorySamplesResponse>;
1938
1881
  }
1939
1882
 
1940
- /**
1941
- * An end user's own consent grants, reachable as `client.grants` — every
1942
- * client this identity has ever authorized, and the means to take one back
1943
- * without touching any of the others.
1944
- *
1945
- * **End-user only.** A server key acts with the app's own full rights and
1946
- * never went through a consent screen itself — there is no "self" here for
1947
- * it to list or revoke, same reasoning as `auth.login` on a `serverKey`
1948
- * client. An end user reaches a group by invitation and signs in through
1949
- * `auth.login` or the hosted login; a server key never does either.
1950
- */
1951
- interface GrantsApi {
1952
- /**
1953
- * Every client this end user has consented to, most-recently-granted
1954
- * first. Each entry carries names, not only ids (`client_name`, `app_name`,
1955
- * `role_name`) — the id is what `revoke()` addresses, but a person
1956
- * deciding whether to revoke something needs to *recognise* it first, and
1957
- * an id is not recognisable. **`client_name` is not trusted** — a
1958
- * self-registered client chooses its own display name, and one has
1959
- * already been measured calling itself "Fleetless Official Helper" — do
1960
- * not render it as if Fleetless vouched for it.
1961
- */
1962
- list(): Promise<ConsentGrantSummary[]>;
1963
- /**
1964
- * Revokes one grant by the client's id — the `client_id` from a
1965
- * `list()` entry — without touching any other client's access.
1966
- *
1967
- * `revoked` tells "nothing matched" (`false`) from "matched and ended"
1968
- * (`true`) — revoking a grant that is already gone (a stale id, a double
1969
- * click) is not an error, just a no-op the caller can still tell apart
1970
- * from a real revocation. `tokens_revoked` counts refresh **families**
1971
- * ended, not access tokens — that count is deliberately never a claim
1972
- * about access tokens, which are stateless and short-lived by design.
1973
- *
1974
- * **What actually happens to an already-issued access token is stronger
1975
- * than that count implies.** It does not simply expire on its own: the
1976
- * cloud re-checks
1977
- * every request for a revoked grant, so **any** currently-valid access
1978
- * token minted through this same client's OAuth flow for this end user
1979
- * — not only the one used to call `revoke()` — answers `401
1980
- * token_revoked` on its very next request, no wait for its TTL. This is
1981
- * scoped to that one client: a session that never went through this
1982
- * client's consent (a plain `auth.login()` session, or one bound to a
1983
- * different client) carries no trace of this client's id and is
1984
- * unaffected. Do not build on the weaker "it will expire eventually" —
1985
- * if this platform ever moves to stateless verification without that
1986
- * re-check, this immediacy would go away silently, which is exactly why
1987
- * it is written down here instead of left implied.
1988
- */
1989
- revoke(clientId: string): Promise<ConsentRevokeResponse>;
1990
- }
1991
-
1992
1883
  /**
1993
1884
  * Robot-wide job reads, reachable as `client.jobs`. Everything here is
1994
1885
  * addressed by robot rather than by slug, which is what `actions` and
@@ -2043,8 +1934,8 @@ interface PublishersApi {
2043
1934
  * mean the SDK protects a caller who stops calling `publish` on purpose
2044
1935
  * without stopping cleanly (e.g. no repeated call at a safe rate): the
2045
1936
  * safety pattern for *how often* and *when* to publish belongs in the
2046
- * app, not here. See the README's "No teleop helpers" section before
2047
- * building a publisher-driven control loop.
1937
+ * app, not here. See the README's "Publishers, and no teleop helpers"
1938
+ * section before building a publisher-driven control loop.
2048
1939
  *
2049
1940
  * Rejects `publisher_busy` while a different user is publishing and has
2050
1941
  * not been quiet for its configured quiet timeout yet — whoever publishes
@@ -2142,10 +2033,12 @@ interface FleetlessClientConfig {
2142
2033
  interface FleetlessClient {
2143
2034
  /** The settled configuration, including the defaults `createClient` filled in. */
2144
2035
  readonly config: FleetlessClientConfig;
2145
- /** Logging in, logging out, and reading who the caller currently is. */
2036
+ /**
2037
+ * The whole client auth API: registration, verification, login, logout,
2038
+ * password reset, invitations, the app's federated sign-in providers, the
2039
+ * MCP consent screen, and the app user's own standing MCP grants.
2040
+ */
2146
2041
  readonly auth: AuthApi;
2147
- /** An end user's own consent grants — not available on a `serverKey` client, same reasoning as `auth`'s session-only methods. */
2148
- readonly grants: GrantsApi;
2149
2042
  /** A topic's latest value, a live subscription to it, and its recorded history. */
2150
2043
  readonly datapoints: DatapointsApi;
2151
2044
  /** Long-running work on the robot: invoke, cancel, and watch a job as it runs. */
@@ -2176,8 +2069,8 @@ interface FleetlessClient {
2176
2069
  }
2177
2070
  /**
2178
2071
  * Builds a client for one app. Pass `tokenStore` (or nothing — the default
2179
- * keeps the session in memory) for an end-user client that logs in with
2180
- * `auth.login` or the hosted login; pass `serverKey` for a server-side
2072
+ * keeps the session in memory) for an app-user client that signs in with
2073
+ * `auth.login` or a federated provider; pass `serverKey` for a server-side
2181
2074
  * caller that never holds a user session. Passing both throws, because the
2182
2075
  * two are different identities and a client acts as exactly one.
2183
2076
  *
@@ -2186,4 +2079,4 @@ interface FleetlessClient {
2186
2079
  */
2187
2080
  declare function createClient(options: FleetlessClientOptions): FleetlessClient;
2188
2081
 
2189
- export { type ActionsApi, type Asset, type AssetBytes, type AssetListResponse, type AssetsApi, type AuthApi, type BeginHostedLoginOptions, type BusyDetails, type CameraDescriptor, type CameraLiveSession, type CameraSnapshot, type CameraSnapshotMeta, type CamerasApi, type ClientIdentity, type CompleteHostedLoginOptions, type ConsentGrantSummary, type ConsentRevokeResponse, type CreateMeshLoaderOptions, type DatapointEvent, type DatapointSubscription, type DatapointSubscriptionHandlers, type DatapointValue, type DatapointsApi, type FleetlessClient, type FleetlessClientConfig, type FleetlessClientOptions, FleetlessError, type FleetlessErrorCode, type FleetlessErrorOptions, type GrantsApi, type HistoryAggregation, type HistoryBucketsResponse, type HistoryOptions, type HistorySamplesResponse, type HostedLoginRequest, InMemoryTokenStore, type InvokeOptions, type Job, type JobEvent, type JobState, type JobSubscription, type JobSubscriptionHandlers, type JobsApi, type LogoutResult, type MeshLoaderDelegate, type ParameterInvalidDetails, type ParameterViolation, type PrepareUrdfSceneOptions, type PublishersApi, type RateLimitDetails, SDK_ERROR_CODES, type SdkErrorCode, type SendCommandOptions, type ServicesApi, type StoredSession, type TokenStore, type UrdfCompleteness, type UrdfSceneManager, type UrdfSceneResources, createClient, parameterInvalidDetails };
2082
+ export { type AcceptInvitationOptions, type ActionsApi, type Asset, type AssetBytes, type AssetListResponse, type AssetsApi, type AuthApi, type BeginOidcLoginOptions, type BusyDetails, type CameraDescriptor, type CameraLiveSession, type CameraSnapshot, type CameraSnapshotMeta, type CamerasApi, type ClientIdentity, type ClientMcpInteraction, type ClientOidcErrorCode, type CompleteOidcLoginOptions, type CreateMeshLoaderOptions, type DatapointEvent, type DatapointSubscription, type DatapointSubscriptionHandlers, type DatapointValue, type DatapointsApi, type FleetlessClient, type FleetlessClientConfig, type FleetlessClientOptions, FleetlessError, type FleetlessErrorCode, type FleetlessErrorOptions, type HistoryAggregation, type HistoryBucketsResponse, type HistoryOptions, type HistorySamplesResponse, InMemoryTokenStore, type InvokeOptions, type Job, type JobEvent, type JobState, type JobSubscription, type JobSubscriptionHandlers, type JobsApi, type McpConsentGrant, type McpInteractionDecision, type MeshLoaderDelegate, type OidcLoginRequest, type ParameterInvalidDetails, type ParameterViolation, type PrepareUrdfSceneOptions, type ProviderButton, type PublishersApi, type RateLimitDetails, type RegisterOptions, SDK_ERROR_CODES, type SdkErrorCode, type SendCommandOptions, type ServicesApi, type StoredSession, type TokenStore, type UrdfCompleteness, type UrdfSceneManager, type UrdfSceneResources, createClient, parameterInvalidDetails };