@fleetless/sdk 1.0.0 → 2.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.ts CHANGED
@@ -119,7 +119,7 @@ type BusyDetails = z.infer<typeof busyDetails>;
119
119
  /**
120
120
  * The REST read of one datapoint. For bridge-captured data `timestamp_ms`
121
121
  * is the capture time at the bridge (spec §6.3); for the cloud-observed
122
- * built-in `bridge-state` it is the time the cloud observed the state.
122
+ * built-in `bridge_state` it is the time the cloud observed the state.
123
123
  */
124
124
  declare const datapointValue: z.ZodObject<{
125
125
  slug: z.ZodString;
@@ -185,7 +185,7 @@ declare const cameraDescriptor: z.ZodObject<{
185
185
  width: z.ZodNumber;
186
186
  height: z.ZodNumber;
187
187
  fps: z.ZodNumber;
188
- snapshot_interval_ms: z.ZodNumber;
188
+ snapshot_interval_seconds: z.ZodNumber;
189
189
  }, z.core.$strip>;
190
190
  type CameraDescriptor = z.infer<typeof cameraDescriptor>;
191
191
  /**
@@ -241,7 +241,7 @@ type HistoryBucketsResponse = z.infer<typeof historyBucketsResponse>;
241
241
  /**
242
242
  * One datapoint sample pushed to a subscriber. The current value arrives
243
243
  * immediately on subscribe, then every change. `timestamp_ms` semantics as
244
- * in `datapointValue` (capture time; cloud-observed for `bridge-state`).
244
+ * in `datapointValue` (capture time; cloud-observed for `bridge_state`).
245
245
  */
246
246
  declare const datapointEvent: z.ZodObject<{
247
247
  type: z.ZodLiteral<"datapoint">;
@@ -263,62 +263,57 @@ declare const sessionTokens: z.ZodObject<{
263
263
  expires_in: z.ZodNumber;
264
264
  }, z.core.$strip>;
265
265
  type SessionTokens = z.infer<typeof sessionTokens>;
266
+
266
267
  /**
267
- * An invitation carries its own accept URL: the link is the primary path
268
- * (the developer shares it), mail is the second. A cloud with no SMTP
269
- * configured still issues invitations — it just cannot send them, and says so
270
- * via `mail_sent`.
271
- */
272
- /**
273
- * What happened to the mail, in three words instead of one (W6c).
268
+ * **What logout can and cannot end, said in three separable facts (W9c,
269
+ * DEF-098).**
274
270
  *
275
- * `mail_sent: boolean` could not tell **"we have no SMTP configured"** from
276
- * **"we tried and the server refused"**, so the console had to pick a sentence
277
- * for a cause it could not know and picked the reassuring one, because a
278
- * link-only invitation is a normal outcome and a bounced one is not. The two
279
- * need opposite actions from whoever reads them: configure a mail server, or
280
- * go and look at why the existing one rejected the message.
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
277
  *
282
- * - `sent` — the SMTP server accepted the message. Not "delivered":
283
- * no sender can promise that, and this value must never
284
- * be rendered as if it could.
285
- * - `not_configured` — no SMTP is set up. **An expected state, not a failure**
286
- * (spec §3.2: invitations work without mail; the link is
287
- * the primary path). The console must not show it as an
288
- * error.
289
- * - `failed` — SMTP was configured, was tried, and refused or was
290
- * unreachable. This one is worth someone's attention.
291
- */
292
- declare const mailStatus: z.ZodEnum<{
293
- failed: "failed";
294
- sent: "sent";
295
- not_configured: "not_configured";
296
- }>;
297
- type MailStatus = z.infer<typeof mailStatus>;
298
- /**
299
- * What registering answers — deliberately **the same for an address that is
300
- * new and one that already has an account**.
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.
301
281
  *
302
- * Anything else is an account-enumeration oracle on an unauthenticated route,
303
- * the same reasoning `passwordResetRequest` carries. An address that already
304
- * exists still gets a mail, saying so; the caller cannot tell which mail was
305
- * sent, and there is nothing in this response to tell them.
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.
306
288
  *
307
- * `mail` is safe to return because it describes **the server's configuration**,
308
- * not the address: `not_configured` means this deployment has no SMTP, which
309
- * is true regardless of who registered. Note that a deployment with no mail
310
- * server cannot complete a self-registration at all the link is the only way
311
- * through, unlike an invitation, where a developer can hand it over directly.
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`.
296
+ *
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*.
312
301
  */
313
- declare const clientRegisterResponse: z.ZodObject<{
314
- mail: z.ZodEnum<{
315
- failed: "failed";
316
- sent: "sent";
317
- not_configured: "not_configured";
318
- }>;
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">;
319
315
  }, z.core.$strip>;
320
- type ClientRegisterResponse = z.infer<typeof clientRegisterResponse>;
321
-
316
+ type ClientLogoutResponse = z.infer<typeof clientLogoutResponse>;
322
317
  /**
323
318
  * Who the caller turned out to be. Returned by the "who am I" endpoint and by
324
319
  * the realtime `auth_ok` frame, so a client can render a session without
@@ -334,6 +329,25 @@ type ClientRegisterResponse = z.infer<typeof clientRegisterResponse>;
334
329
  * `app_id` and `role_id` are null for them, and roles do not filter what they
335
330
  * see. `kind` states this explicitly rather than leaving it to be inferred
336
331
  * from which id happens to be set.
332
+ *
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.
341
+ *
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.
337
351
  */
338
352
  declare const clientIdentity: z.ZodObject<{
339
353
  kind: z.ZodEnum<{
@@ -347,6 +361,9 @@ declare const clientIdentity: z.ZodObject<{
347
361
  app_id: z.ZodNullable<z.ZodUUID>;
348
362
  role_id: z.ZodNullable<z.ZodUUID>;
349
363
  email: z.ZodNullable<z.ZodEmail>;
364
+ act: z.ZodOptional<z.ZodObject<{
365
+ admin_user_id: z.ZodUUID;
366
+ }, z.core.$strict>>;
350
367
  }, z.core.$strip>;
351
368
  type ClientIdentity = z.infer<typeof clientIdentity>;
352
369
 
@@ -393,9 +410,43 @@ type Asset = z.infer<typeof asset>;
393
410
  declare const urdfCompleteness: z.ZodObject<{
394
411
  present: z.ZodBoolean;
395
412
  mesh_count: z.ZodNumber;
396
- missing: z.ZodArray<z.ZodString>;
413
+ missing: z.ZodArray<z.ZodObject<{
414
+ uri: z.ZodString;
415
+ element: z.ZodEnum<{
416
+ mesh: "mesh";
417
+ texture: "texture";
418
+ }>;
419
+ }, z.core.$strip>>;
397
420
  }, z.core.$strip>;
398
421
  type UrdfCompleteness = z.infer<typeof urdfCompleteness>;
422
+ declare const assetSyncStatus: z.ZodObject<{
423
+ sync_id: z.ZodUUID;
424
+ robot_id: z.ZodUUID;
425
+ state: z.ZodEnum<{
426
+ running: "running";
427
+ succeeded: "succeeded";
428
+ failed: "failed";
429
+ }>;
430
+ done: z.ZodNumber;
431
+ total: z.ZodNumber;
432
+ failed: z.ZodArray<z.ZodObject<{
433
+ reference: z.ZodString;
434
+ kind: z.ZodEnum<{
435
+ unresolvable: "unresolvable";
436
+ upload_failed: "upload_failed";
437
+ refused: "refused";
438
+ too_large: "too_large";
439
+ }>;
440
+ details: z.ZodOptional<z.ZodNullable<z.ZodObject<{
441
+ limit_bytes: z.ZodNumber;
442
+ size_bytes: z.ZodNumber;
443
+ }, z.core.$strip>>>;
444
+ }, z.core.$strip>>;
445
+ reason: z.ZodNullable<z.ZodString>;
446
+ started_at: z.ZodISODateTime;
447
+ updated_at: z.ZodISODateTime;
448
+ }, z.core.$strip>;
449
+ type AssetSyncStatus = z.infer<typeof assetSyncStatus>;
399
450
  declare const assetListResponse: z.ZodObject<{
400
451
  assets: z.ZodArray<z.ZodObject<{
401
452
  id: z.ZodUUID;
@@ -412,10 +463,43 @@ declare const assetListResponse: z.ZodObject<{
412
463
  sha256: z.ZodString;
413
464
  created_at: z.ZodISODateTime;
414
465
  }, z.core.$strip>>;
466
+ active_sync: z.ZodNullable<z.ZodObject<{
467
+ sync_id: z.ZodUUID;
468
+ robot_id: z.ZodUUID;
469
+ state: z.ZodEnum<{
470
+ running: "running";
471
+ succeeded: "succeeded";
472
+ failed: "failed";
473
+ }>;
474
+ done: z.ZodNumber;
475
+ total: z.ZodNumber;
476
+ failed: z.ZodArray<z.ZodObject<{
477
+ reference: z.ZodString;
478
+ kind: z.ZodEnum<{
479
+ unresolvable: "unresolvable";
480
+ upload_failed: "upload_failed";
481
+ refused: "refused";
482
+ too_large: "too_large";
483
+ }>;
484
+ details: z.ZodOptional<z.ZodNullable<z.ZodObject<{
485
+ limit_bytes: z.ZodNumber;
486
+ size_bytes: z.ZodNumber;
487
+ }, z.core.$strip>>>;
488
+ }, z.core.$strip>>;
489
+ reason: z.ZodNullable<z.ZodString>;
490
+ started_at: z.ZodISODateTime;
491
+ updated_at: z.ZodISODateTime;
492
+ }, z.core.$strip>>;
415
493
  urdf: z.ZodObject<{
416
494
  present: z.ZodBoolean;
417
495
  mesh_count: z.ZodNumber;
418
- missing: z.ZodArray<z.ZodString>;
496
+ missing: z.ZodArray<z.ZodObject<{
497
+ uri: z.ZodString;
498
+ element: z.ZodEnum<{
499
+ mesh: "mesh";
500
+ texture: "texture";
501
+ }>;
502
+ }, z.core.$strip>>;
419
503
  }, z.core.$strip>;
420
504
  urdf_available: z.ZodNullable<z.ZodBoolean>;
421
505
  }, z.core.$strip>;
@@ -459,9 +543,79 @@ type ParameterInvalidDetails = z.infer<typeof parameterInvalidDetails>;
459
543
  * list is the shared vocabulary, not a closed set, so a new refusal never
460
544
  * needs a contracts release before it can be reported honestly.
461
545
  */
462
- declare const ERROR_CODES: readonly ["not_found", "validation_error", "bad_request", "unknown_datapoint", "invalid_token", "protocol_mismatch", "invalid_frame", "duplicate_slug", "reserved_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", "not_a_member", "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"];
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"];
463
547
  type ErrorCode = (typeof ERROR_CODES)[number];
464
548
 
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
+
465
619
  /**
466
620
  * Codes the SDK produces itself rather than relaying from the server. Kept
467
621
  * out of `@fleetless/contracts`' `ERROR_CODES` deliberately — that list is
@@ -771,7 +925,15 @@ interface UrdfSceneResources {
771
925
  */
772
926
  urdfText: string;
773
927
  /**
774
- * The same reference strings `assets.list()`'s `urdf.missing` reports.
928
+ * The same entries `assets.list()`'s `urdf.missing` reports — verbatim,
929
+ * not reduced to bare strings (W9b, DEF-081). Each carries `element`
930
+ * (`'mesh' | 'texture'`) alongside `uri`: before this, both kinds arrived
931
+ * as an undifferentiated `string[]` and a caller could only ever say "N
932
+ * meshes missing", wrongly, for a URDF whose gap was actually a texture —
933
+ * the same defect W9b fixed one layer up, in the console. Reducing this
934
+ * field back to `string[]` here would throw the distinction away again at
935
+ * exactly the point a caller would render it. If you only need the URIs,
936
+ * `missing.map(m => m.uri)`.
775
937
  *
776
938
  * **Top-level only — not a `.dae`'s internal references (Kassandra-W7a
777
939
  * review correction).** The cloud builds this list from the URDF text
@@ -791,7 +953,7 @@ interface UrdfSceneResources {
791
953
  * reference NOT in this list that still fails to load at render time is a
792
954
  * different failure — the store answered but the fetch itself did not.
793
955
  */
794
- missing: string[];
956
+ missing: UrdfCompleteness['missing'];
795
957
  /**
796
958
  * Revokes every `blob:` URL this call created. Call once the scene has
797
959
  * finished loading (success or failure) or on unmount — safe to call more
@@ -816,6 +978,28 @@ interface AssetsApi {
816
978
  * workspace by hand, the URIs are what they can act on.
817
979
  */
818
980
  list(robotId: string): Promise<AssetListResponse>;
981
+ /**
982
+ * The status of one sync by id — for **reconnecting** to a sync already in
983
+ * flight, not for starting one (W9b, DEF-147).
984
+ *
985
+ * **Starting a sync stays out of this SDK, on purpose (spec §4.6):**
986
+ * "übertragen wird ausschließlich auf explizite Anforderung des
987
+ * Entwicklers über die Console" — a developer action, Owner-tier, done
988
+ * once, from the console. This method is a different thing: `list()`'s
989
+ * `active_sync` (or a `busy` refusal's `assetSyncBusyDetails`) hands a
990
+ * caller a `sync_id` for a sync that is **already running**, and before
991
+ * this method existed there was no way for anything built on this SDK to
992
+ * do anything with that id except throw it away. That was the actual
993
+ * shape of DEF-147: the id was recoverable server-side the whole time
994
+ * (`GET .../assets/sync/{id}` has existed since W7) — nothing reachable
995
+ * from this SDK ever called it.
996
+ *
997
+ * A page reload is the case this exists for: whatever held the `sync_id`
998
+ * in memory is gone, `list()` (or a fresh `busy` refusal) hands it back,
999
+ * and this is how a caller resumes watching the same sync instead of
1000
+ * either losing the progress bar or being told to start a second one.
1001
+ */
1002
+ syncStatus(robotId: string, syncId: string): Promise<AssetSyncStatus>;
819
1003
  /** One asset's bytes by id — a mesh, or any asset directly, addressed the same way `createMeshLoader` reaches one internally. */
820
1004
  get(robotId: string, assetId: string): Promise<AssetBytes>;
821
1005
  /**
@@ -1026,17 +1210,46 @@ interface BeginHostedLoginOptions {
1026
1210
  redirectUri: string;
1027
1211
  scope?: string;
1028
1212
  /**
1029
- * RFC 8707 audience binding (register row, W7b→W7c): the resource this
1030
- * session's token should be usable against an MCP endpoint
1031
- * (`/mcp/<app>`, W7c) or another audience-checking resource this
1032
- * deployment validates. **Omit it for an ordinary app login.** A token
1033
- * with no `resource` carries no `aud` and works unrestricted against this
1034
- * app's own REST surface exactly as it always has; that path is
1035
- * unaffected by this field's existence. Only a caller that is itself
1036
- * going to present the token to an audience-checking resource needs to
1037
- * ask for one and must ask for the *right* one, because `/mcp/<app>`
1038
- * refuses a token whose `aud` names a different app exactly as hard as it
1039
- * refuses a token with none at all.
1213
+ * RFC 8707 audience binding (register row, W7b): the resource this
1214
+ * session's token should be usable against. **Omit it for an ordinary app
1215
+ * login.** A token with no `resource` carries no `aud` and works
1216
+ * unrestricted against this app's own REST surface exactly as it always
1217
+ * has; that path is unaffected by this field's existence. Only a caller
1218
+ * that is itself going to present the token to an audience-checking
1219
+ * resource needs to ask for one.
1220
+ *
1221
+ * **`/oauth/authorize` accepts exactly two shapes** anything else is
1222
+ * refused with `invalid_target` on the redirect back, before a code is
1223
+ * ever issued (`cloud/src/routes/oauth.ts`, `isKnownResource`):
1224
+ *
1225
+ * - `<base>/mcp-stub/resource` — the global OAuth resource stub, matched
1226
+ * byte-for-byte.
1227
+ * - `<base>/mcp-stub/resource/<app_identifier>` — the per-app stub, and
1228
+ * only for **the calling client's own app**. This is an existence check
1229
+ * plus an ownership check, not a shape check: the app must exist *and*
1230
+ * be the one this `clientId` is registered to, so a client on app A can
1231
+ * never be minted a token whose `aud` names app B.
1232
+ *
1233
+ * Both are served and validated by
1234
+ * `cloud/src/routes/oauth-resource-stub.ts`, whose validator refuses a
1235
+ * token with **no** `aud` exactly as hard as one with the wrong `aud` —
1236
+ * "unscoped" must never read as "for me" — comparing by equality, never
1237
+ * by prefix.
1238
+ *
1239
+ * **`<base>/mcp/<app_identifier>` is not a resource any more.** There is
1240
+ * no per-app MCP endpoint: the cloud registers one central, non-parametric
1241
+ * `POST /mcp` (`cloud/src/routes/mcp.ts`, contracts'
1242
+ * `MCP_ENDPOINT_PATH`), and the `/oauth/authorize` branch that used to
1243
+ * accept a `/mcp/<app>` resource was deleted along with the app-level
1244
+ * `mcp_enabled` flag. Asking for one now yields `invalid_target` for
1245
+ * every app, **including your own**. The central MCP endpoint has its own
1246
+ * OAuth flow (`cloud/src/routes/mcp-oauth.ts`) which this SDK's hosted
1247
+ * login does not drive — `beginHostedLogin` always targets
1248
+ * `OAUTH_PATHS.authorize`.
1249
+ *
1250
+ * Whatever you name here is re-checked at the token exchange: it must
1251
+ * match what the code was authorized for, and it must still name a
1252
+ * resource this client may be issued a token for.
1040
1253
  */
1041
1254
  resource?: string;
1042
1255
  }
@@ -1089,6 +1302,54 @@ interface CompleteHostedLoginOptions {
1089
1302
  */
1090
1303
  resource?: string;
1091
1304
  }
1305
+ /**
1306
+ * What `logout()` resolves with (W9c, DEF-098) — five separable facts, not
1307
+ * one nullable URL, mirroring the wire's `clientLogoutResponse`.
1308
+ *
1309
+ * `idp_logout` is `null` exactly when `revoked` is `false`: the server call
1310
+ * that would have produced it never got a chance to answer (no local
1311
+ * session, or the request itself failed), so there is nothing to report —
1312
+ * not even "not federated", because this client never learned that either.
1313
+ * When `revoked` is `true` and a real response came back, `idp_logout` is
1314
+ * one of:
1315
+ *
1316
+ * - `{ status: 'redirect', url }` — send the browser here to end the
1317
+ * session at the IdP too. Nothing else in this SDK does that navigation
1318
+ * for you (same "this SDK is thin" reasoning as `cameras.live`).
1319
+ * - `{ status: 'not_federated' }` — this session never came from an IdP;
1320
+ * there is nothing else to end.
1321
+ * - `{ status: 'unsupported_by_idp' }` — it did, and the IdP publishes no
1322
+ * `end_session_endpoint` (RP-initiated logout is optional in OIDC).
1323
+ * - `{ status: 'hint_unavailable' }` — it did, the IdP *can* end the
1324
+ * session, and Fleetless has nothing to ask it with: the stored
1325
+ * `id_token_hint` could not be decrypted, or the session predates the fix
1326
+ * that started keeping one.
1327
+ * - `{ status: 'session_unknown' }` — the server did not find this session at
1328
+ * all (the token was unknown, already superseded, revoked, or expired), so
1329
+ * it can say **nothing** about an IdP.
1330
+ *
1331
+ * **`unsupported_by_idp` and `hint_unavailable` both mean the IdP session
1332
+ * survives and this platform cannot end it** — do not render either one the
1333
+ * same as `not_federated`; that reports a session as fully ended when it
1334
+ * isn't.
1335
+ *
1336
+ * **`session_unknown` is a different kind of nothing, and the reason it
1337
+ * exists is a second logout** (W9 review). A double click, a repeated POST, an
1338
+ * app that logs out on unmount *and* on a route change: the last call wins,
1339
+ * and before this outcome existed it answered `not_federated` — a positive
1340
+ * claim about an IdP the server had never looked up. An app that treats it as
1341
+ * *"fully logged out"* skips a redirect the **first** call may well have
1342
+ * returned, and the user stays signed in at the IdP after clicking log out.
1343
+ *
1344
+ * Note that `revoked` cannot help you here: it reports whether the HTTP call
1345
+ * succeeded, not whether a session was found — so this case arrives as
1346
+ * `{ revoked: true, idp_logout: { status: 'session_unknown' } }`, never as the
1347
+ * `null` below.
1348
+ */
1349
+ interface LogoutResult {
1350
+ revoked: boolean;
1351
+ idp_logout: ClientLogoutResponse['idp_logout'] | null;
1352
+ }
1092
1353
  interface AuthApi {
1093
1354
  /** Exchanges email + password, and the client's configured app identifier, for a session. */
1094
1355
  login(email: string, password: string): Promise<void>;
@@ -1197,64 +1458,40 @@ interface AuthApi {
1197
1458
  * change, a block, or a membership removal take effect on the very next
1198
1459
  * request without a fresh token), not a bug — but a kiosk or shared
1199
1460
  * workstation needs to know that number.
1200
- */
1201
- logout(): Promise<{
1202
- revoked: boolean;
1203
- }>;
1204
- /** Who the caller turned out to be, without decoding a token client-side. */
1205
- me(): Promise<ClientIdentity>;
1206
- /**
1207
- * Starts self-registration into this app's pool (spec §3.2, W6c) — the
1208
- * second of the pool's two entry paths, alongside a console invitation.
1209
- * Sets the password here; `confirmRegistration` below only ever sees a
1210
- * token.
1211
- *
1212
- * **This does not log the caller in — there is no session yet.** A domain
1213
- * filter says *which* domains may register, never *whether the caller
1214
- * owns the address*, so minting a session here would let anybody who
1215
- * knows an allowed domain register as somebody else at it and receive
1216
- * whatever role the app assigns — which on this platform can mean
1217
- * permission to move a robot. So this mails a confirmation link and waits;
1218
- * `confirmRegistration(token)` is what proves the address and returns a
1219
- * session, the same two-step shape as `requestPasswordReset` /
1220
- * `confirmPasswordReset` below.
1221
1461
  *
1222
- * **Resolves identically for an address that is new and one that already
1223
- * has an account** same account-enumeration reasoning as
1224
- * `requestPasswordReset`. `mail` in the resolved value describes this
1225
- * deployment's mail configuration, not the address, so it is safe to
1226
- * read but note that a deployment with no SMTP (`mail:
1227
- * 'not_configured'`) cannot complete a self-registration at all: unlike
1228
- * an invitation, where a developer can hand the link over directly, the
1229
- * confirmation link has no other channel.
1462
+ * **Nor does it end a federated session at the identity provider (W9c,
1463
+ * DEF-098).** Ending the Fleetless session and ending the IdP session are
1464
+ * two different things see `LogoutResult.idp_logout`. This method does
1465
+ * not act on that information itself (no redirect, no fetch to the IdP);
1466
+ * it only reports what the server found, the same "this SDK stays thin"
1467
+ * boundary as everywhere else (`cameras.live` hands back a URL and a
1468
+ * token and stops there too).
1230
1469
  *
1231
- * **The role is not yours to choose.** There is no `role` parameter: the
1232
- * app's own `selfRegistration.role_id` decides it (§3.2 a pool member
1233
- * has exactly one role per app), and a signature that accepted one would
1234
- * be offering a choice the platform has to refuse anyway.
1470
+ * **`idp_logout` is `null` when there was no server answer to report**
1471
+ * and that is *not* the same as `revoked === false`.
1235
1472
  *
1236
- * **Fails the same way whether self-registration is off or the address's
1237
- * domain is not permitted.** Those two refusals are deliberately
1238
- * indistinguishable, for the same reason as above: telling them apart
1239
- * would let a caller probe configuration that is not theirs to see. Do
1240
- * not build a UI that tries to tell "this app doesn't allow sign-ups"
1241
- * apart from "your email domain isn't allowed" from the error alone.
1473
+ * This block used to state the biconditional (*"`null` exactly when
1474
+ * `revoked` is `false`"*), and **it is measurably wrong** (Argus-W9, W9
1475
+ * review): calling `logout()` with **no local session** returns
1476
+ * `{ revoked: true, idp_logout: null }`. The implementation says so six
1477
+ * lines above itself *"Nothing to revoke: no server-side session lingers,
1478
+ * so this counts as revoked but there is no IdP fact to report either"* —
1479
+ * so the public docblock and the code contradicted each other in one file.
1242
1480
  *
1243
- * Behind the same limiter as `login` (`rate_limited`, W6c) creating a
1244
- * row is the cheapest attack this platform has to defend against, so
1245
- * expect this to be throttled first under load, not last.
1246
- */
1247
- register(email: string, password: string): Promise<ClientRegisterResponse>;
1248
- /**
1249
- * Spends a self-registration confirmation token and returns the new
1250
- * session — the counterpart to `register` above. Stores the session
1251
- * exactly like `login`, so a caller who has just confirmed is not then
1252
- * told to log in separately.
1481
+ * That mattered more than a wrong sentence usually does, because **this is
1482
+ * the rule the text tells a caller to branch on**. Someone who reads
1483
+ * `revoked === true` and trusts the promise does not expect `null`.
1484
+ * TypeScript catches it (the type is `| null`); a JavaScript caller does
1485
+ * not.
1253
1486
  *
1254
- * The token is single-use and expires; using it twice, or too late,
1255
- * answers `token_spent` either way, same as `confirmPasswordReset` below.
1487
+ * So: **`null` means this client had nothing to send or the request never
1488
+ * answered.** It is not `not_federated`, which is a real finding about a
1489
+ * real session — conflating the two is the ambiguity this shape exists to
1490
+ * remove, and it is still the reason to read both fields rather than one.
1256
1491
  */
1257
- confirmRegistration(token: string): Promise<void>;
1492
+ logout(): Promise<LogoutResult>;
1493
+ /** Who the caller turned out to be, without decoding a token client-side. */
1494
+ me(): Promise<ClientIdentity>;
1258
1495
  /**
1259
1496
  * Changes the current end user's password (spec §3, W6c).
1260
1497
  *
@@ -1280,33 +1517,17 @@ interface AuthApi {
1280
1517
  */
1281
1518
  changePassword(currentPassword: string, newPassword: string): Promise<void>;
1282
1519
  /**
1283
- * Requests a password-reset link for `email` (spec §3, W6c) sent by
1284
- * mail; the console-issued link is the primary path, this is the
1285
- * self-service one.
1286
- *
1287
- * **Resolves the same way whether or not `email` belongs to an account,
1288
- * and always did — this is not a bug to work around.** It is the one
1289
- * unauthenticated route in this SDK where revealing existence would be an
1290
- * account-enumeration oracle (see `passwordResetRequest`'s doc comment in
1291
- * `@fleetless/contracts`). There is nothing in this method's return value
1292
- * or its error codes that distinguishes "sent" from "no such account" —
1293
- * do not build a UI branch for "no such account" here, because there is
1294
- * nothing to branch on, on purpose.
1295
- */
1296
- requestPasswordReset(email: string): Promise<void>;
1297
- /**
1298
- * Completes a password reset using the token from the emailed link (spec
1299
- * §3, W6c).
1520
+ * The hosted password-reset page, served by the platform on the API
1521
+ * origin (`/reset-password`). An app links a user there; nothing is
1522
+ * called. The two former methods that posted to `/api/client/password/…`
1523
+ * are gone with the routes they named (2.0.0).
1300
1524
  *
1301
- * The token is single-use and expires; using it twice, or too late,
1302
- * answers `token_spent` either way deliberately one code for both (see
1303
- * `ERROR_CODES` in `@fleetless/contracts`): telling them apart would tell
1304
- * a stranger whether a token ever existed, and the recovery is the same
1305
- * regardless — request a new link. Succeeding revokes every session of
1306
- * that identity, the same as `changePassword` — a forgotten password is
1307
- * one of the two states where somebody else may be holding a session.
1525
+ * The page owns the whole flow it takes the address, mails a
1526
+ * single-use link, and takes the new password on the way back — so there
1527
+ * is nothing for an app to sequence and nothing here that could reveal
1528
+ * whether an address belongs to an account.
1308
1529
  */
1309
- confirmPasswordReset(token: string, newPassword: string): Promise<void>;
1530
+ passwordResetUrl(): string;
1310
1531
  }
1311
1532
 
1312
1533
  /**
@@ -1531,6 +1752,58 @@ interface DatapointsApi {
1531
1752
  }): Promise<HistorySamplesResponse>;
1532
1753
  }
1533
1754
 
1755
+ /**
1756
+ * An end user's own consent grants (spec §3.4/§17, W9c, DEF-099) — every
1757
+ * client this identity has ever authorized, and the means to take one back
1758
+ * without touching any of the others.
1759
+ *
1760
+ * **End-user only.** A server key acts with the app's own full rights and
1761
+ * never went through a consent screen itself — there is no "self" here for
1762
+ * it to list or revoke, same reasoning as `auth.login`/`auth.register` on a
1763
+ * `serverKey` client.
1764
+ */
1765
+ interface GrantsApi {
1766
+ /**
1767
+ * Every client this end user has consented to, most-recently-granted
1768
+ * first. Each entry carries names, not only ids (`client_name`, `app_name`,
1769
+ * `role_name`) — the id is what `revoke()` addresses, but a person
1770
+ * deciding whether to revoke something needs to *recognise* it first, and
1771
+ * an id is not recognisable. **`client_name` is not trusted** — a
1772
+ * self-registered client chooses its own display name (W7c already
1773
+ * measured what that buys: one called itself "Fleetless Official
1774
+ * Helper") — do not render it as if Fleetless vouched for it.
1775
+ */
1776
+ list(): Promise<ConsentGrantSummary[]>;
1777
+ /**
1778
+ * Revokes one grant by the client's id — the `client_id` from a
1779
+ * `list()` entry — without touching any other client's access.
1780
+ *
1781
+ * `revoked` tells "nothing matched" (`false`) from "matched and ended"
1782
+ * (`true`) — revoking a grant that is already gone (a stale id, a double
1783
+ * click) is not an error, just a no-op the caller can still tell apart
1784
+ * from a real revocation. `tokens_revoked` counts refresh **families**
1785
+ * ended, not access tokens — that count is deliberately never a claim
1786
+ * about access tokens, which are stateless and short-lived by design.
1787
+ *
1788
+ * **What actually happens to an already-issued access token is stronger
1789
+ * than that count implies, and it was measured, not assumed (W9c gate
1790
+ * step 1, 2026-08-19, end to end through this SDK against a real
1791
+ * cloud).** It does not simply expire on its own: the cloud re-checks
1792
+ * every request for a revoked grant, so **any** currently-valid access
1793
+ * token minted through this same client's OAuth flow for this end user
1794
+ * — not only the one used to call `revoke()` — answers `401
1795
+ * token_revoked` on its very next request, no wait for its TTL. This is
1796
+ * scoped to that one client: a session that never went through this
1797
+ * client's consent (a plain `auth.login()` session, or one bound to a
1798
+ * different client) carries no trace of this client's id and is
1799
+ * unaffected. Do not build on the weaker "it will expire eventually" —
1800
+ * if this platform ever moves to stateless verification without that
1801
+ * re-check, this immediacy would go away silently, which is exactly why
1802
+ * it is written down here instead of left implied.
1803
+ */
1804
+ revoke(clientId: string): Promise<ConsentRevokeResponse>;
1805
+ }
1806
+
1534
1807
  interface JobsApi {
1535
1808
  /**
1536
1809
  * Every job the platform currently believes this robot has — `GET
@@ -1653,6 +1926,8 @@ interface FleetlessClientConfig {
1653
1926
  interface FleetlessClient {
1654
1927
  readonly config: FleetlessClientConfig;
1655
1928
  readonly auth: AuthApi;
1929
+ /** An end user's own consent grants (spec §3.4/§17, W9c) — not available on a `serverKey` client, same reasoning as `auth`'s session-only methods. */
1930
+ readonly grants: GrantsApi;
1656
1931
  readonly datapoints: DatapointsApi;
1657
1932
  readonly actions: ActionsApi;
1658
1933
  readonly services: ServicesApi;
@@ -1678,4 +1953,4 @@ interface FleetlessClient {
1678
1953
  }
1679
1954
  declare function createClient(options: FleetlessClientOptions): FleetlessClient;
1680
1955
 
1681
- 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 ClientRegisterResponse, type CompleteHostedLoginOptions, 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, type HostedLoginRequest, InMemoryTokenStore, type Job, type JobEvent, type JobState, type JobSubscription, type JobSubscriptionHandlers, type JobsApi, type MailStatus, 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 };
1956
+ 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 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 };