@fleetless/sdk 2.0.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +108 -0
- package/README.md +105 -59
- package/dist/index.cjs +4796 -1240
- package/dist/index.d.cts +881 -756
- package/dist/index.d.ts +881 -756
- package/dist/index.js +4796 -1240
- package/package.json +5 -5
package/dist/index.d.cts
CHANGED
|
@@ -18,9 +18,9 @@ import { z } from 'zod';
|
|
|
18
18
|
* admits it lost track.
|
|
19
19
|
*/
|
|
20
20
|
declare const jobState: z.ZodEnum<{
|
|
21
|
+
failed: "failed";
|
|
21
22
|
running: "running";
|
|
22
23
|
succeeded: "succeeded";
|
|
23
|
-
failed: "failed";
|
|
24
24
|
cancelled: "cancelled";
|
|
25
25
|
lost: "lost";
|
|
26
26
|
}>;
|
|
@@ -30,9 +30,9 @@ declare const job: z.ZodObject<{
|
|
|
30
30
|
robot_id: z.ZodUUID;
|
|
31
31
|
slug: z.ZodString;
|
|
32
32
|
state: z.ZodEnum<{
|
|
33
|
+
failed: "failed";
|
|
33
34
|
running: "running";
|
|
34
35
|
succeeded: "succeeded";
|
|
35
|
-
failed: "failed";
|
|
36
36
|
cancelled: "cancelled";
|
|
37
37
|
lost: "lost";
|
|
38
38
|
}>;
|
|
@@ -65,9 +65,9 @@ declare const jobEvent: z.ZodObject<{
|
|
|
65
65
|
robot_id: z.ZodUUID;
|
|
66
66
|
slug: z.ZodString;
|
|
67
67
|
state: z.ZodEnum<{
|
|
68
|
+
failed: "failed";
|
|
68
69
|
running: "running";
|
|
69
70
|
succeeded: "succeeded";
|
|
70
|
-
failed: "failed";
|
|
71
71
|
cancelled: "cancelled";
|
|
72
72
|
lost: "lost";
|
|
73
73
|
}>;
|
|
@@ -97,9 +97,9 @@ declare const busyDetails: z.ZodObject<{
|
|
|
97
97
|
robot_id: z.ZodUUID;
|
|
98
98
|
slug: z.ZodString;
|
|
99
99
|
state: z.ZodEnum<{
|
|
100
|
+
failed: "failed";
|
|
100
101
|
running: "running";
|
|
101
102
|
succeeded: "succeeded";
|
|
102
|
-
failed: "failed";
|
|
103
103
|
cancelled: "cancelled";
|
|
104
104
|
lost: "lost";
|
|
105
105
|
}>;
|
|
@@ -256,6 +256,9 @@ type DatapointEvent = z.infer<typeof datapointEvent>;
|
|
|
256
256
|
* Access plus refresh (spec §3.4). 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;
|
|
@@ -265,105 +268,149 @@ declare const sessionTokens: z.ZodObject<{
|
|
|
265
268
|
type SessionTokens = z.infer<typeof sessionTokens>;
|
|
266
269
|
|
|
267
270
|
/**
|
|
268
|
-
* **
|
|
269
|
-
*
|
|
271
|
+
* **Why a federated sign-in ended without a session, in a code the app can
|
|
272
|
+
* branch on** — carried back to the app's own `redirect_uri` as `error`, not
|
|
273
|
+
* rendered by Fleetless (D2). The only Fleetless-rendered page in this flow is
|
|
274
|
+
* the one for a state that can no longer be resolved to a redirect URI, because
|
|
275
|
+
* then there is nowhere to send the answer.
|
|
270
276
|
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
+
* The five rows of D4's table are the first five values plus `no_access`:
|
|
278
|
+
*
|
|
279
|
+
* - `no_access` — the identity is unknown and nothing admits it, or the account
|
|
280
|
+
* it names is not `active`. **One code for both**, because to the person the
|
|
281
|
+
* remedy is the same — ask somebody to let you in — and a code that split an
|
|
282
|
+
* outcome nobody acts on differently would tell a stranger which half applied.
|
|
283
|
+
* - `email_taken` — the address already belongs to another app user, and the
|
|
284
|
+
* provider is not permitted to link (`link_verified_emails`, or the provider
|
|
285
|
+
* did not assert `email_verified`). Deliberately not `no_access`: the remedy
|
|
286
|
+
* is different — *sign in the way you signed up*.
|
|
287
|
+
* - `email_unverified` — the provider asserted an address without
|
|
288
|
+
* `email_verified`. **An unverified address never produces or links an
|
|
289
|
+
* account**, whatever the rest of the policy says.
|
|
290
|
+
* - `domain_not_allowed`, `registration_closed` — the self-registration policy
|
|
291
|
+
* refused. Honest, because neither is about whether a person exists.
|
|
292
|
+
* - `idp_unavailable`, `exchange_failed`, `claims_incomplete`,
|
|
293
|
+
* `provider_misconfigured`, `provider_disabled` — the provider's or the
|
|
294
|
+
* developer's to fix, and the app can say so.
|
|
295
|
+
* - `invalid_request` — the start parameters did not hold up.
|
|
296
|
+
* - `quota_exceeded` — the org has as many app users as its `max_end_users`
|
|
297
|
+
* quota allows, so no account can be created for this identity. Named rather
|
|
298
|
+
* than folded into `no_access`, for `domain_not_allowed`'s reason: it is not
|
|
299
|
+
* about the person, the app can say what happened, and the remedy belongs to
|
|
300
|
+
* the developer rather than to whoever is trying to sign in. It is raised
|
|
301
|
+
* **only where an account would be created** — an identity that already has
|
|
302
|
+
* one signs in at the quota exactly as it does under it, because refusing a
|
|
303
|
+
* sign-in would turn a protection limit into an outage.
|
|
304
|
+
*/
|
|
305
|
+
declare const clientOidcErrorCode: z.ZodEnum<{
|
|
306
|
+
no_access: "no_access";
|
|
307
|
+
email_taken: "email_taken";
|
|
308
|
+
email_unverified: "email_unverified";
|
|
309
|
+
domain_not_allowed: "domain_not_allowed";
|
|
310
|
+
registration_closed: "registration_closed";
|
|
311
|
+
idp_unavailable: "idp_unavailable";
|
|
312
|
+
exchange_failed: "exchange_failed";
|
|
313
|
+
claims_incomplete: "claims_incomplete";
|
|
314
|
+
provider_misconfigured: "provider_misconfigured";
|
|
315
|
+
provider_disabled: "provider_disabled";
|
|
316
|
+
invalid_request: "invalid_request";
|
|
317
|
+
quota_exceeded: "quota_exceeded";
|
|
318
|
+
}>;
|
|
319
|
+
type ClientOidcErrorCode = z.infer<typeof clientOidcErrorCode>;
|
|
320
|
+
/**
|
|
321
|
+
* **A pending MCP authorization, as the app's own consent screen reads it**
|
|
322
|
+
* (D7). Fleetless renders no page here either: `authorize` redirects to the
|
|
323
|
+
* app's `mcp_login_url` with an interaction id, the app authenticates the user
|
|
324
|
+
* with its normal UI, shows this, and approves or denies through the API.
|
|
277
325
|
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
326
|
+
* `client_name_verified` is `z.literal(false)`, and that is the whole point of
|
|
327
|
+
* the field. The name comes from an **unauthenticated** dynamic registration —
|
|
328
|
+
* the client typed it about itself, nobody checked it — so a consent screen
|
|
329
|
+
* that rendered it as though it were an identity would be teaching people to
|
|
330
|
+
* trust a string an attacker chooses. A literal rather than a boolean because
|
|
331
|
+
* there is no verified case to distinguish: an app that reads this field at all
|
|
332
|
+
* has to handle the untrusted one, and a `true` branch would be dead code
|
|
333
|
+
* pretending to be a safeguard.
|
|
334
|
+
*/
|
|
335
|
+
declare const clientMcpInteraction: z.ZodObject<{
|
|
336
|
+
id: z.ZodString;
|
|
337
|
+
app_id: z.ZodUUID;
|
|
338
|
+
client_name: z.ZodNullable<z.ZodString>;
|
|
339
|
+
client_name_verified: z.ZodLiteral<false>;
|
|
340
|
+
scopes: z.ZodArray<z.ZodString>;
|
|
341
|
+
already_granted: z.ZodBoolean;
|
|
342
|
+
expires_at: z.ZodISODateTime;
|
|
343
|
+
}, z.core.$strip>;
|
|
344
|
+
type ClientMcpInteraction = z.infer<typeof clientMcpInteraction>;
|
|
345
|
+
/**
|
|
346
|
+
* **One standing MCP consent, as both withdrawal doors list it.**
|
|
281
347
|
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
* them apart makes the choice for everyone.
|
|
348
|
+
* A grant is what lets a later authorization skip the app's consent screen:
|
|
349
|
+
* `clientMcpInteraction.already_granted` is a read of exactly this row. It is
|
|
350
|
+
* written when a person approves and it is removed by neither the client's
|
|
351
|
+
* registration lapsing nor its access token expiring — so without a door it
|
|
352
|
+
* was a decision a person could make once and never unmake.
|
|
288
353
|
*
|
|
289
|
-
* **
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
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`.
|
|
354
|
+
* **Standing only.** A withdrawn grant is stamped rather than deleted, so the
|
|
355
|
+
* store still holds it; neither listing returns one. The question both doors
|
|
356
|
+
* ask is *what is connected right now*, and a row that answered "connected,
|
|
357
|
+
* but no" would be a state every caller has to filter for itself.
|
|
296
358
|
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
359
|
+
* `client_name_verified` is `z.literal(false)` for the reason
|
|
360
|
+
* `clientMcpInteraction` gives at length: the name comes from an
|
|
361
|
+
* unauthenticated dynamic registration, the client chose it about itself, and
|
|
362
|
+
* a list that rendered it as an identity would be teaching people to trust a
|
|
363
|
+
* string an attacker picked. Here it matters more than on the consent screen,
|
|
364
|
+
* not less — a "connected apps" list is read long after the moment of
|
|
365
|
+
* approval, when nobody remembers what they clicked.
|
|
301
366
|
*/
|
|
302
|
-
declare const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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">;
|
|
367
|
+
declare const mcpConsentGrant: z.ZodObject<{
|
|
368
|
+
client_id: z.ZodString;
|
|
369
|
+
client_name: z.ZodNullable<z.ZodString>;
|
|
370
|
+
client_name_verified: z.ZodLiteral<false>;
|
|
371
|
+
granted_at: z.ZodISODateTime;
|
|
315
372
|
}, z.core.$strip>;
|
|
316
|
-
type
|
|
373
|
+
type McpConsentGrant = z.infer<typeof mcpConsentGrant>;
|
|
317
374
|
/**
|
|
318
375
|
* Who the caller turned out to be. Returned by the "who am I" endpoint and by
|
|
319
376
|
* the realtime `auth_ok` frame, so a client can render a session without
|
|
320
377
|
* decoding a token itself — decoding a JWT in the client is how apps end up
|
|
321
378
|
* trusting claims nobody verified.
|
|
322
379
|
*
|
|
323
|
-
* **Three kinds of caller reach the client API
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
* tab) subscribe on `/realtime` as one. A developer is **org-scoped, not
|
|
380
|
+
* **Three kinds of caller reach the client API.** Besides app users and server
|
|
381
|
+
* keys, a **developer** does: the console's playground runs over the real
|
|
382
|
+
* client API and appears in the audit as the developer, and the console's own
|
|
383
|
+
* live views subscribe on `/realtime` as one. A developer is **org-scoped, not
|
|
328
384
|
* app-scoped** — they own the configuration of every robot in their org — so
|
|
329
385
|
* `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
|
-
*
|
|
386
|
+
* see. `kind` states this explicitly rather than leaving it to be inferred from
|
|
387
|
+
* which id happens to be set.
|
|
332
388
|
*
|
|
333
|
-
* **`
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
*
|
|
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.
|
|
389
|
+
* **`end_user_id` became `app_user_id`, and that is a rename with a meaning.**
|
|
390
|
+
* The old subject was a member of the org's one pool, reachable through an
|
|
391
|
+
* assignment; the new one is a row that belongs to exactly one app. Renaming
|
|
392
|
+
* rather than keeping the key is deliberate: a consumer reading `.end_user_id`
|
|
393
|
+
* would have typechecked and meant something subtly different, which is the
|
|
394
|
+
* quietest way for a cut like this to go wrong.
|
|
341
395
|
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
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.
|
|
396
|
+
* **`act` is gone.** It named the org admin behind an impersonation (the RFC
|
|
397
|
+
* 8693 pattern). Impersonation is deleted with no successor (D1), so a field
|
|
398
|
+
* that could still arrive would describe a delegation nothing can mint — and a
|
|
399
|
+
* client rendering "you are acting as …" from it would be showing a state the
|
|
400
|
+
* platform cannot enter.
|
|
351
401
|
*/
|
|
352
402
|
declare const clientIdentity: z.ZodObject<{
|
|
353
403
|
kind: z.ZodEnum<{
|
|
354
|
-
server_key: "server_key";
|
|
355
404
|
developer: "developer";
|
|
356
|
-
|
|
405
|
+
server_key: "server_key";
|
|
406
|
+
app_user: "app_user";
|
|
357
407
|
}>;
|
|
358
408
|
developer_id: z.ZodNullable<z.ZodUUID>;
|
|
359
|
-
|
|
409
|
+
app_user_id: z.ZodNullable<z.ZodUUID>;
|
|
360
410
|
server_key_id: z.ZodNullable<z.ZodUUID>;
|
|
361
411
|
app_id: z.ZodNullable<z.ZodUUID>;
|
|
362
412
|
role_id: z.ZodNullable<z.ZodUUID>;
|
|
363
413
|
email: z.ZodNullable<z.ZodEmail>;
|
|
364
|
-
act: z.ZodOptional<z.ZodObject<{
|
|
365
|
-
admin_user_id: z.ZodUUID;
|
|
366
|
-
}, z.core.$strict>>;
|
|
367
414
|
}, z.core.$strip>;
|
|
368
415
|
type ClientIdentity = z.infer<typeof clientIdentity>;
|
|
369
416
|
|
|
@@ -423,9 +470,9 @@ declare const assetSyncStatus: z.ZodObject<{
|
|
|
423
470
|
sync_id: z.ZodUUID;
|
|
424
471
|
robot_id: z.ZodUUID;
|
|
425
472
|
state: z.ZodEnum<{
|
|
473
|
+
failed: "failed";
|
|
426
474
|
running: "running";
|
|
427
475
|
succeeded: "succeeded";
|
|
428
|
-
failed: "failed";
|
|
429
476
|
}>;
|
|
430
477
|
done: z.ZodNumber;
|
|
431
478
|
total: z.ZodNumber;
|
|
@@ -467,9 +514,9 @@ declare const assetListResponse: z.ZodObject<{
|
|
|
467
514
|
sync_id: z.ZodUUID;
|
|
468
515
|
robot_id: z.ZodUUID;
|
|
469
516
|
state: z.ZodEnum<{
|
|
517
|
+
failed: "failed";
|
|
470
518
|
running: "running";
|
|
471
519
|
succeeded: "succeeded";
|
|
472
|
-
failed: "failed";
|
|
473
520
|
}>;
|
|
474
521
|
done: z.ZodNumber;
|
|
475
522
|
total: z.ZodNumber;
|
|
@@ -543,84 +590,14 @@ type ParameterInvalidDetails = z.infer<typeof parameterInvalidDetails>;
|
|
|
543
590
|
* list is the shared vocabulary, not a closed set, so a new refusal never
|
|
544
591
|
* needs a contracts release before it can be reported honestly.
|
|
545
592
|
*/
|
|
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", "
|
|
593
|
+
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
594
|
type ErrorCode = (typeof ERROR_CODES)[number];
|
|
548
595
|
|
|
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
596
|
/**
|
|
620
597
|
* Codes the SDK produces itself rather than relaying from the server. Kept
|
|
621
598
|
* out of `@fleetless/contracts`' `ERROR_CODES` deliberately — that list is
|
|
622
599
|
* the *wire* vocabulary, every entry something a server may actually send,
|
|
623
|
-
* and none of these are.
|
|
600
|
+
* and none of these are.
|
|
624
601
|
*
|
|
625
602
|
* - `no_session` / `no_websocket`: a client-side refusal *before* a request
|
|
626
603
|
* ever reaches the network (not logged in; no WebSocket implementation
|
|
@@ -628,102 +605,109 @@ type ConsentRevokeResponse = z.infer<typeof consentRevokeResponse>;
|
|
|
628
605
|
* caller can tell "the server refused me" from "the SDK refused before
|
|
629
606
|
* asking" by the code alone.
|
|
630
607
|
* - `unparseable_error`: the opposite direction — a real response *did*
|
|
631
|
-
* arrive, its body just
|
|
632
|
-
* refusal at all, just "we
|
|
633
|
-
* - `command_timeout
|
|
634
|
-
*
|
|
608
|
+
* arrive, its body just was not shaped like the platform's error format.
|
|
609
|
+
* Not a refusal at all, just "we do not know what the server said."
|
|
610
|
+
* - `command_timeout`: a realtime command (`invoke`/`cancel`/`publish`) got
|
|
611
|
+
* no `command_result` within its timeout. The server may still answer
|
|
635
612
|
* later on the same socket — nobody knows — but the caller cannot be made
|
|
636
613
|
* to wait forever for that.
|
|
637
|
-
* - `command_outcome_unknown
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
614
|
+
* - `command_outcome_unknown`: worse than a timeout, and told apart from it
|
|
615
|
+
* on purpose — the realtime connection that carried the command was
|
|
616
|
+
* replaced by a new one (a reconnect) before any reply arrived. A reply
|
|
617
|
+
* can now never come: the server, if it answered at all, answered a
|
|
641
618
|
* socket that no longer exists. The command may or may not have run.
|
|
642
619
|
* Never retried automatically — that could run an action twice — the
|
|
643
620
|
* caller recovers by reading the job (e.g. `actions.subscribe`), since
|
|
644
621
|
* state is observed by slug regardless of which connection asked for it.
|
|
645
|
-
* - `unexpected_response
|
|
622
|
+
* - `unexpected_response`: the server answered `ok:true` but left out
|
|
646
623
|
* something the command is defined to always return (e.g. no `job` on a
|
|
647
624
|
* successful `invoke`) — a contract violation the SDK noticed, not a
|
|
648
625
|
* refusal.
|
|
649
|
-
* - `invalid_option
|
|
650
|
-
*
|
|
651
|
-
* `timeoutMs < patienceMs` on `invoke`/`call`
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
*
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
*
|
|
675
|
-
*
|
|
676
|
-
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
* this
|
|
698
|
-
* call had already created (`blob:` URLs) is revoked before this throws —
|
|
699
|
-
* an aborted load must not leak what it fetched before the signal fired,
|
|
700
|
-
* the same guarantee a failed load already had (D6).
|
|
701
|
-
* - `state_mismatch` (W7b): `auth.completeHostedLogin()` was called with a
|
|
702
|
-
* `state` that does not match the `expectedState` its own `beginHostedLogin()`
|
|
703
|
-
* returned for this attempt (or with no `state` at all — `beginHostedLogin`
|
|
626
|
+
* - `invalid_option`: the caller passed an *SDK-level* argument or option
|
|
627
|
+
* that cannot mean what it looks like it means. Three cases so far:
|
|
628
|
+
* `timeoutMs < patienceMs` on `invoke`/`call` — the SDK would give up
|
|
629
|
+
* locally before the platform's own patience runs out, and report
|
|
630
|
+
* `command_timeout` for a call the platform never actually refused; a
|
|
631
|
+
* non-string, non-null, non-omitted `jobId` on `cancel` — almost always a
|
|
632
|
+
* caller who upgraded past the older `cancel(robotId, slug, options?)`
|
|
633
|
+
* signature and is still passing an options object third; and a
|
|
634
|
+
* `concurrency` on `assets.prepareUrdfScene` that is not a positive
|
|
635
|
+
* integer, which would otherwise fetch nothing and return a scene that
|
|
636
|
+
* renders blank with no error to explain why. **A fourth since 3.0.0:**
|
|
637
|
+
* every `auth` method needing an app user's own session, called on a client
|
|
638
|
+
* built with a `serverKey` — `register`, `login`, `logout`, the password
|
|
639
|
+
* and invitation calls, both OIDC calls, the two MCP decisions and the two
|
|
640
|
+
* grant calls. Those threw a bare `Error` before, which a caller could only
|
|
641
|
+
* catch by message. All of them are refused
|
|
642
|
+
* before any request is sent — as a rejection, since every one of those
|
|
643
|
+
* methods is `async`. A client-side mistake to fix, not something a
|
|
644
|
+
* server response could ever produce, which is why this code belongs here
|
|
645
|
+
* and not in `@fleetless/contracts`' `ERROR_CODES`.
|
|
646
|
+
* - `untrusted_absolute_url`: the SDK refused to fetch an absolute URL
|
|
647
|
+
* whose origin does not match this client's own `apiUrl` — thrown before
|
|
648
|
+
* the request is ever sent, so no `Authorization` header is ever built for
|
|
649
|
+
* it, let alone attached. The one caller that fetches an absolute URL at
|
|
650
|
+
* all is `assets.createMeshLoader`, following a URDF's rewritten mesh
|
|
651
|
+
* URIs — and a URDF is ROS graph input, not first-party data, so an app
|
|
652
|
+
* rendering one must not silently trust wherever it points.
|
|
653
|
+
* `assets.createMeshLoader`'s `onComplete` surfaces this the same way it
|
|
654
|
+
* surfaces a network failure: `(null, err)`.
|
|
655
|
+
* - `no_urdf_synced`: `assets.prepareUrdfScene` looked for a `kind: 'urdf'`
|
|
656
|
+
* row in `assets.list()` and found none. Thrown before any asset fetch,
|
|
657
|
+
* rather than left to surface as a confusing downstream failure from
|
|
658
|
+
* `URDFLoader.parse(undefined)` or similar — the caller's fix is "sync a
|
|
659
|
+
* URDF first", which this error can say directly.
|
|
660
|
+
* - `aborted`: `assets.prepareUrdfScene()` was given an `AbortSignal` and it
|
|
661
|
+
* fired — either already-aborted before the call started, or mid-flight
|
|
662
|
+
* while a fetch was in progress. Normalized to this one code regardless of
|
|
663
|
+
* which stage the abort landed in, rather than surfacing whatever shape
|
|
664
|
+
* the underlying `fetch()` rejects an aborted request with (a
|
|
665
|
+
* `DOMException` named `AbortError` in a browser, an `Error` named
|
|
666
|
+
* `AbortError` under Node's `fetch` — two different shapes a caller would
|
|
667
|
+
* otherwise have to detect themselves to tell "I cancelled this" from "the
|
|
668
|
+
* network actually failed"). Every partial resource this call had already
|
|
669
|
+
* created (`blob:` URLs) is revoked before this throws — an aborted load
|
|
670
|
+
* must not leak what it fetched before the signal fired, the same
|
|
671
|
+
* guarantee a failed load already had.
|
|
672
|
+
* - `state_mismatch`: `auth.completeOidcLogin()` was called with a `state`
|
|
673
|
+
* that does not match the `expectedState` its own `beginOidcLogin()`
|
|
674
|
+
* returned for this attempt — or with no `state` at all (`beginOidcLogin`
|
|
704
675
|
* always sets one, so a callback carrying none does not look like a reply
|
|
705
|
-
* to a flow this client started)
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
709
|
-
*
|
|
710
|
-
*
|
|
676
|
+
* to a flow this client started), or with an **empty `expectedState`**,
|
|
677
|
+
* meaning nothing was persisted for this attempt at all. That last case is
|
|
678
|
+
* folded in rather than given its own code: two empty strings compare
|
|
679
|
+
* equal, so it has to be checked explicitly or the comparison defends
|
|
680
|
+
* nothing for exactly the callers most likely to hit it — but a caller
|
|
681
|
+
* branching on the code has the same next step either way, which is to
|
|
682
|
+
* start the sign-in again. Which of the two happened is in the message,
|
|
683
|
+
* because the *developer's* remedies do differ ("check how your app
|
|
684
|
+
* persisted the value" versus "this response belongs to a sign-in you did
|
|
685
|
+
* not start"). Thrown before `/api/client/oidc/exchange` is ever called:
|
|
686
|
+
* RFC 6749 section 10.12's whole point is that a caller must not complete
|
|
687
|
+
* an authorization response it did not itself request, so this check
|
|
688
|
+
* happens client-side, first, rather than being left to the server to
|
|
689
|
+
* catch — by which point a one-time code would already have been spent for
|
|
690
|
+
* a flow this client never started.
|
|
691
|
+
*/
|
|
692
|
+
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"];
|
|
693
|
+
/**
|
|
694
|
+
* The union of `SDK_ERROR_CODES` — the SDK's own client-side error
|
|
695
|
+
* vocabulary. A `FleetlessError` whose `code` is one of these was raised by
|
|
696
|
+
* this SDK rather than relayed from the server.
|
|
711
697
|
*/
|
|
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"];
|
|
713
698
|
type SdkErrorCode = (typeof SDK_ERROR_CODES)[number];
|
|
714
699
|
/**
|
|
715
700
|
* A stable code a caller can branch on: a server-defined code (open-ended —
|
|
716
|
-
* see `ErrorCode`'s own doc comment), one of
|
|
717
|
-
* codes
|
|
701
|
+
* see `ErrorCode`'s own doc comment), one of `SdkErrorCode`'s client-side
|
|
702
|
+
* codes, or, since neither list is exhaustive, any other string.
|
|
718
703
|
* `(string & {})` is the standard trick to keep autocomplete on the known
|
|
719
704
|
* values while still accepting an arbitrary one.
|
|
720
705
|
*/
|
|
721
706
|
type FleetlessErrorCode = ErrorCode | SdkErrorCode | (string & {});
|
|
722
707
|
/**
|
|
723
|
-
* The
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
* parse `message` — it is not part of the contract, only `code` is.
|
|
708
|
+
* The third argument of `FleetlessError`'s constructor: what a thrower can
|
|
709
|
+
* attach beyond the code and the message. Both fields are optional, and
|
|
710
|
+
* both are absent on the codes the SDK raises before any request is sent.
|
|
727
711
|
*/
|
|
728
712
|
interface FleetlessErrorOptions {
|
|
729
713
|
/** Field-level detail for validation errors, passed through verbatim. */
|
|
@@ -731,79 +715,126 @@ interface FleetlessErrorOptions {
|
|
|
731
715
|
/** The HTTP status of the response that produced this error, if any. */
|
|
732
716
|
status?: number;
|
|
733
717
|
}
|
|
718
|
+
/**
|
|
719
|
+
* The one error type the SDK throws for a refused API call: a stable
|
|
720
|
+
* machine-readable `code` a caller can branch on (`forbidden` versus
|
|
721
|
+
* `token_expired`) plus a human `message` for logs and debugging. Never
|
|
722
|
+
* parse `message` — it is not part of the contract, only `code` is.
|
|
723
|
+
*
|
|
724
|
+
* Catch it by shape rather than by class where you can (`err.code`), since
|
|
725
|
+
* a bundler that ends up with two copies of the SDK also ends up with two
|
|
726
|
+
* classes and `instanceof` then answers `false` for a genuine one.
|
|
727
|
+
*/
|
|
734
728
|
declare class FleetlessError extends Error {
|
|
729
|
+
/** What went wrong, as a stable string — the field to branch on. */
|
|
735
730
|
readonly code: FleetlessErrorCode;
|
|
731
|
+
/**
|
|
732
|
+
* Structured detail the server sent with the refusal, if any: the
|
|
733
|
+
* violations behind `parameter_invalid`, the running job behind `busy`,
|
|
734
|
+
* `retry_after_ms` behind `rate_limited`. Parse it rather than assume
|
|
735
|
+
* its shape — `parameterInvalidDetails` is exported for exactly that.
|
|
736
|
+
*/
|
|
736
737
|
readonly details?: unknown;
|
|
738
|
+
/** The HTTP status of the response that produced this error, if it came from one. */
|
|
737
739
|
readonly status?: number;
|
|
740
|
+
/**
|
|
741
|
+
* Builds an error. `code` is what a caller branches on and `message` is
|
|
742
|
+
* for a human; anything else the refusal carried goes in `options`.
|
|
743
|
+
*/
|
|
738
744
|
constructor(code: FleetlessErrorCode, message: string, options?: FleetlessErrorOptions);
|
|
739
745
|
}
|
|
740
746
|
|
|
747
|
+
/**
|
|
748
|
+
* The options every realtime command accepts — `actions.cancel` and
|
|
749
|
+
* `publishers.publish` take exactly these; `InvokeOptions` extends them for
|
|
750
|
+
* `actions.invoke` and `services.call`.
|
|
751
|
+
*/
|
|
741
752
|
interface SendCommandOptions {
|
|
742
753
|
/** How long to wait for a `command_result` before rejecting `command_timeout`. Default 10s. */
|
|
743
754
|
timeoutMs?: number;
|
|
744
755
|
}
|
|
745
756
|
/**
|
|
746
|
-
* `invoke
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
* long as it runs and is observed, not awaited).
|
|
750
|
-
*
|
|
751
|
-
* Optional; absent means the platform's own default (`DEFAULT_PATIENCE_MS`,
|
|
752
|
-
* 15s) — exactly today's behaviour for a caller who names no preference.
|
|
753
|
-
* Outside `[MIN_PATIENCE_MS, MAX_PATIENCE_MS]` the platform refuses with
|
|
754
|
-
* `validation_error` rather than clamping, and this SDK does not clamp
|
|
755
|
-
* locally or retry — surfacing the refusal is the whole of what it does
|
|
756
|
-
* with this field. The floor exists because impatience reaches the robot,
|
|
757
|
-
* not just the platform: a patience too short to survive a goal-acceptance
|
|
758
|
-
* round trip made the bridge report `goal_timeout` and then issue a
|
|
759
|
-
* *corrective cancel* against a goal an action server accepted a moment
|
|
760
|
-
* later — a caller who names an unreachable deadline was causing a real
|
|
761
|
-
* cancellation on the machine, repeatably, not just receiving an error.
|
|
762
|
-
*
|
|
763
|
-
* `timeoutMs` bounds how long *this SDK* waits locally for a reply on the
|
|
764
|
-
* wire it already sent on; `patienceMs` travels to the platform and bounds
|
|
765
|
-
* what *it* is willing to wait for from the robot. **They are not set
|
|
766
|
-
* independently of each other, and D3a exists because the first version of
|
|
767
|
-
* this doc comment said they were.** `timeoutMs` left unset is *derived*
|
|
768
|
-
* from `patienceMs` (see `resolveLocalWaitMs`), not defaulted to a fixed
|
|
769
|
-
* number that might be shorter — the SDK giving up locally before the
|
|
770
|
-
* platform's own deadline would report `command_timeout` for a call the
|
|
771
|
-
* platform never actually refused, which is exactly the two-clocks defect
|
|
772
|
-
* this wave exists to remove, just relocated into this SDK instead of
|
|
773
|
-
* between the cloud and the bridge. Setting both explicitly with
|
|
774
|
-
* `timeoutMs < patienceMs` is refused with `invalid_option` before any
|
|
775
|
-
* request is sent, for the same reason.
|
|
757
|
+
* What `actions.invoke` and `services.call` accept on top of
|
|
758
|
+
* `SendCommandOptions`: the two clocks a command runs under, one local to
|
|
759
|
+
* this SDK and one on the platform.
|
|
776
760
|
*/
|
|
777
761
|
interface InvokeOptions extends SendCommandOptions {
|
|
762
|
+
/**
|
|
763
|
+
* How long **the platform itself** should wait for this one call before
|
|
764
|
+
* giving up on the robot — the whole wait for a service call, goal
|
|
765
|
+
* *acceptance* only for an action (once accepted, a job runs as long as it
|
|
766
|
+
* runs and is observed, not awaited).
|
|
767
|
+
*
|
|
768
|
+
* Optional; absent means the platform's own `DEFAULT_PATIENCE_MS`, 15s —
|
|
769
|
+
* exactly the behaviour of a caller who names no preference. Outside
|
|
770
|
+
* `MIN_PATIENCE_MS` to `MAX_PATIENCE_MS` (1s to 120s, both exported by
|
|
771
|
+
* `@fleetless/contracts`) the platform refuses with `validation_error`
|
|
772
|
+
* rather than clamping, and this SDK does not clamp locally or retry:
|
|
773
|
+
* surfacing the refusal is the whole of what it does with this field.
|
|
774
|
+
* The floor exists because
|
|
775
|
+
* impatience reaches the robot, not just the platform — a patience too
|
|
776
|
+
* short to survive a goal-acceptance round trip made the bridge report
|
|
777
|
+
* `goal_timeout` and then issue a *corrective cancel* against a goal an
|
|
778
|
+
* action server accepted a moment later, so a caller who names an
|
|
779
|
+
* unreachable deadline was causing a real cancellation on the machine,
|
|
780
|
+
* repeatably, not just receiving an error.
|
|
781
|
+
*
|
|
782
|
+
* `timeoutMs` bounds how long *this SDK* waits locally for a reply on the
|
|
783
|
+
* wire it already sent on; `patienceMs` travels to the platform and bounds
|
|
784
|
+
* what *it* is willing to wait for from the robot. **They are not set
|
|
785
|
+
* independently of each other.** `timeoutMs` left unset is *derived* from
|
|
786
|
+
* `patienceMs`, not defaulted to a fixed number that might be shorter —
|
|
787
|
+
* the SDK giving up locally before the platform's own deadline would
|
|
788
|
+
* report `command_timeout` for a call the platform never actually refused.
|
|
789
|
+
* Setting both explicitly with `timeoutMs < patienceMs` is refused with
|
|
790
|
+
* `invalid_option` before any request is sent, for the same reason.
|
|
791
|
+
*/
|
|
778
792
|
patienceMs?: number;
|
|
779
793
|
}
|
|
780
794
|
|
|
795
|
+
/**
|
|
796
|
+
* The callbacks `actions.subscribe` reports through: one for every job
|
|
797
|
+
* update on the slug, one for a refusal of the subscription itself.
|
|
798
|
+
*/
|
|
781
799
|
interface JobSubscriptionHandlers {
|
|
782
800
|
/** Called on every update pushed for the slug's current job — state, feedback, progress and result. */
|
|
783
801
|
onJob(event: JobEvent): void;
|
|
784
|
-
/** Called once if the subscription is refused
|
|
802
|
+
/** Called once if the subscription is refused, e.g. `forbidden` or an unknown slug. */
|
|
785
803
|
onError?(error: FleetlessError): void;
|
|
786
804
|
}
|
|
805
|
+
/** A live job subscription, returned by `actions.subscribe`. */
|
|
787
806
|
interface JobSubscription {
|
|
788
|
-
/**
|
|
807
|
+
/**
|
|
808
|
+
* Stops this subscription. The `unsubscribe` frame reaches the server only
|
|
809
|
+
* when this was the **last** holder of the robot and slug pair, and only if
|
|
810
|
+
* the channel is connected — subscriptions are reference-counted across
|
|
811
|
+
* kinds, so releasing this one while a second `actions.subscribe`, a
|
|
812
|
+
* `datapoints.subscribe` or an in-flight `services.call` still holds the
|
|
813
|
+
* same pair leaves that one's stream running untouched. Safe to call more
|
|
814
|
+
* than once.
|
|
815
|
+
*/
|
|
789
816
|
unsubscribe(): void;
|
|
790
817
|
}
|
|
791
818
|
|
|
819
|
+
/**
|
|
820
|
+
* Long-running work on a robot, reachable as `client.actions`. An action is
|
|
821
|
+
* a ROS action the developer exposed under a slug: it is invoked, it runs
|
|
822
|
+
* for as long as it runs, and it reports back while it does.
|
|
823
|
+
*/
|
|
792
824
|
interface ActionsApi {
|
|
793
825
|
/**
|
|
794
|
-
* Invokes an action
|
|
826
|
+
* Invokes an action. Resolves as soon as the job is created
|
|
795
827
|
* — the job id is informative, not the result. Feedback, progress and the
|
|
796
828
|
* eventual result arrive separately over `subscribe`. A second invoke of
|
|
797
829
|
* the same slug while one is already running is refused `busy`, with
|
|
798
830
|
* `error.details.running` naming the job that is running.
|
|
799
831
|
*
|
|
800
|
-
* `options.patienceMs` bounds goal *acceptance* only
|
|
832
|
+
* `options.patienceMs` bounds goal *acceptance* only — once a goal
|
|
801
833
|
* is accepted this call has already resolved; the job then runs as long
|
|
802
834
|
* as it runs, observed via `subscribe`, never awaited. `options.timeoutMs`
|
|
803
835
|
* (this SDK's own local wait for the acceptance reply) is derived from
|
|
804
836
|
* `patienceMs` when left unset, and the combination `timeoutMs <
|
|
805
|
-
* patienceMs` is refused with `invalid_option` rather than raced
|
|
806
|
-
* `resolveLocalWaitMs` in `commands.ts`, D3a.
|
|
837
|
+
* patienceMs` is refused with `invalid_option` rather than raced.
|
|
807
838
|
*/
|
|
808
839
|
invoke(robotId: string, slug: string, params: Record<string, unknown>, options?: InvokeOptions): Promise<Job>;
|
|
809
840
|
/**
|
|
@@ -811,10 +842,9 @@ interface ActionsApi {
|
|
|
811
842
|
* Resolves with the `Job` the cancel was actually sent to, or `null` if
|
|
812
843
|
* nothing matched.
|
|
813
844
|
*
|
|
814
|
-
* **Two different requests, both legitimate
|
|
845
|
+
* **Two different requests, both legitimate:**
|
|
815
846
|
* - `cancel(robotId, slug)` — no `jobId` — is the operator's stop button:
|
|
816
|
-
* whatever is running on this slug, stop it.
|
|
817
|
-
* before W6b.
|
|
847
|
+
* whatever is running on this slug, stop it.
|
|
818
848
|
* - `cancel(robotId, slug, jobId)` cancels **that** job specifically. If
|
|
819
849
|
* it is not the one running, the platform answers `not_found` — this
|
|
820
850
|
* never silently falls back to stopping whatever *is* running, because
|
|
@@ -830,7 +860,7 @@ interface ActionsApi {
|
|
|
830
860
|
cancel(robotId: string, slug: string, jobId?: string | null, options?: SendCommandOptions): Promise<Job | null>;
|
|
831
861
|
/**
|
|
832
862
|
* Subscribes to the slug's job: state, feedback, progress and result, as
|
|
833
|
-
* they happen. State is observed **by slug**, not by job id
|
|
863
|
+
* they happen. State is observed **by slug**, not by job id — this
|
|
834
864
|
* is what makes late delivery after a reconnect and a second observer
|
|
835
865
|
* watching the same job both work without special-casing either. Naming a
|
|
836
866
|
* job to `cancel` does not change this: a slug is still a *place a job may
|
|
@@ -841,7 +871,9 @@ interface ActionsApi {
|
|
|
841
871
|
|
|
842
872
|
/** An asset's bytes plus its declared media type — the shape `assets.get` answers with. */
|
|
843
873
|
interface AssetBytes {
|
|
874
|
+
/** The asset's raw bytes, exactly as stored. */
|
|
844
875
|
body: Uint8Array;
|
|
876
|
+
/** The declared media type, or `null` if the store did not record one. */
|
|
845
877
|
mime: string | null;
|
|
846
878
|
}
|
|
847
879
|
/**
|
|
@@ -856,6 +888,7 @@ interface AssetBytes {
|
|
|
856
888
|
* not only that one library.
|
|
857
889
|
*/
|
|
858
890
|
type MeshLoaderDelegate = (path: string, manager: unknown, material: unknown, onComplete: (obj: unknown | null, err?: Error) => void) => void;
|
|
891
|
+
/** What `assets.createMeshLoader` accepts beyond the robot and the delegate. */
|
|
859
892
|
interface CreateMeshLoaderOptions {
|
|
860
893
|
/**
|
|
861
894
|
* How long to wait for `delegate`'s `onComplete` before giving up.
|
|
@@ -867,12 +900,14 @@ interface CreateMeshLoaderOptions {
|
|
|
867
900
|
timeoutMs?: number;
|
|
868
901
|
}
|
|
869
902
|
/**
|
|
870
|
-
* three.js's own `LoadingManager.setURLModifier(callback)` shape
|
|
903
|
+
* three.js's own `LoadingManager.setURLModifier(callback)` shape — the
|
|
904
|
+
* only thing `assets.prepareUrdfScene` needs from a three.js
|
|
905
|
+
* `LoadingManager`, so a caller passes theirs straight in.
|
|
871
906
|
* Every load the manager oversees is routed through `callback` first — not
|
|
872
907
|
* only the loader you handed the manager to, but every loader it constructs
|
|
873
908
|
* internally on the same manager (`ColladaLoader`'s own `TextureLoader` for
|
|
874
909
|
* a `.dae`'s `<init_from>` images, in particular). That is the one hook
|
|
875
|
-
* that exists
|
|
910
|
+
* that exists one level up, for everything, where `createMeshLoader`'s
|
|
876
911
|
* per-loader `loadMeshCb` override does not reach: `TextureLoader` has no
|
|
877
912
|
* override hook of its own.
|
|
878
913
|
*
|
|
@@ -880,8 +915,10 @@ interface CreateMeshLoaderOptions {
|
|
|
880
915
|
* or `urdf-loader`, same discipline as `MeshLoaderDelegate` above.
|
|
881
916
|
*/
|
|
882
917
|
interface UrdfSceneManager {
|
|
918
|
+
/** Installs a callback every load through this manager is routed through first. */
|
|
883
919
|
setURLModifier(callback: (url: string) => string): unknown;
|
|
884
920
|
}
|
|
921
|
+
/** What `assets.prepareUrdfScene` accepts beyond the robot and the manager. */
|
|
885
922
|
interface PrepareUrdfSceneOptions {
|
|
886
923
|
/**
|
|
887
924
|
* How many assets to fetch in parallel. Default 6 — a default that keeps
|
|
@@ -889,24 +926,23 @@ interface PrepareUrdfSceneOptions {
|
|
|
889
926
|
* meshes), not a number with a sweep behind it. Pass your own if you have
|
|
890
927
|
* a reason to.
|
|
891
928
|
*
|
|
892
|
-
* Must be a positive integer — `0` or negative
|
|
893
|
-
* rather than silently fetching nothing and returning a
|
|
894
|
-
* renders completely blank with no error to explain why.
|
|
929
|
+
* Must be a positive integer — `0` or negative rejects with
|
|
930
|
+
* `invalid_option` rather than silently fetching nothing and returning a
|
|
931
|
+
* scene that renders completely blank with no error to explain why.
|
|
895
932
|
*/
|
|
896
933
|
concurrency?: number;
|
|
897
934
|
/**
|
|
898
|
-
* Cancels this call
|
|
935
|
+
* Cancels this call — a caller who navigates away
|
|
899
936
|
* or switches to a different robot mid-load can abort every in-flight
|
|
900
937
|
* fetch this method has started, not merely stop it from starting new
|
|
901
938
|
* ones. Checked before the first request; if it fires while a request is
|
|
902
|
-
* already in progress,
|
|
903
|
-
*
|
|
904
|
-
*
|
|
905
|
-
* background.
|
|
939
|
+
* already in progress, the SDK forwards it straight to `fetch()`, so the
|
|
940
|
+
* connection itself is torn down, not just abandoned by this SDK while it
|
|
941
|
+
* keeps running in the background.
|
|
906
942
|
*
|
|
907
943
|
* Every `blob:` URL already created before the abort is revoked before
|
|
908
944
|
* this call rejects with `FleetlessError('aborted', ...)` — the same
|
|
909
|
-
* guarantee a load that fails outright already had
|
|
945
|
+
* guarantee a load that fails outright already had: an aborted load
|
|
910
946
|
* must not leak what it had already fetched.
|
|
911
947
|
*
|
|
912
948
|
* `manager`'s URL modifier is only ever installed once every asset has
|
|
@@ -916,6 +952,10 @@ interface PrepareUrdfSceneOptions {
|
|
|
916
952
|
*/
|
|
917
953
|
signal?: AbortSignal;
|
|
918
954
|
}
|
|
955
|
+
/**
|
|
956
|
+
* What `assets.prepareUrdfScene` resolves with: the URDF text to parse, what
|
|
957
|
+
* the sync could not resolve, and the cleanup for everything it fetched.
|
|
958
|
+
*/
|
|
919
959
|
interface UrdfSceneResources {
|
|
920
960
|
/**
|
|
921
961
|
* The robot's URDF as raw text — `package://` URIs intact, not rewritten
|
|
@@ -926,24 +966,21 @@ interface UrdfSceneResources {
|
|
|
926
966
|
urdfText: string;
|
|
927
967
|
/**
|
|
928
968
|
* The same entries `assets.list()`'s `urdf.missing` reports — verbatim,
|
|
929
|
-
* not reduced to bare strings
|
|
969
|
+
* not reduced to bare strings. Each carries `element`
|
|
930
970
|
* (`'mesh' | 'texture'`) alongside `uri`: before this, both kinds arrived
|
|
931
971
|
* 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
|
-
*
|
|
934
|
-
*
|
|
972
|
+
* meshes missing", wrongly, for a URDF whose gap was actually a texture.
|
|
973
|
+
* Reducing this field back to `string[]` here would throw the distinction
|
|
974
|
+
* away again at
|
|
935
975
|
* exactly the point a caller would render it. If you only need the URIs,
|
|
936
976
|
* `missing.map(m => m.uri)`.
|
|
937
977
|
*
|
|
938
|
-
* **Top-level only — not a `.dae`'s internal references
|
|
939
|
-
*
|
|
978
|
+
* **Top-level only — not a `.dae`'s internal references.** The cloud
|
|
979
|
+
* builds this list from the URDF text
|
|
940
980
|
* alone (`<mesh>`/`<texture>` `filename` attributes), which is the only
|
|
941
981
|
* place it can see without parsing every `.dae` a sync touches; it never
|
|
942
|
-
* has and never can include an internal `<init_from>` reference.
|
|
943
|
-
*
|
|
944
|
-
* as a correction rather than silently widening the sentence, since a
|
|
945
|
-
* caller who trusted "both" to mean both would build a completeness check
|
|
946
|
-
* against a list that structurally cannot report the second half. A
|
|
982
|
+
* has and never can include an internal `<init_from>` reference. Do not
|
|
983
|
+
* build a completeness check on it as though it covered both: a
|
|
947
984
|
* `.dae`-internal reference the sync could not resolve surfaces through
|
|
948
985
|
* the sync's own failure reporting instead, not here.
|
|
949
986
|
*
|
|
@@ -955,44 +992,53 @@ interface UrdfSceneResources {
|
|
|
955
992
|
*/
|
|
956
993
|
missing: UrdfCompleteness['missing'];
|
|
957
994
|
/**
|
|
958
|
-
* Revokes every
|
|
959
|
-
* finished loading (success or failure) or on unmount
|
|
960
|
-
* than once.
|
|
995
|
+
* Revokes every object URL this call created that carries bytes. Call
|
|
996
|
+
* once the scene has finished loading (success or failure) or on unmount
|
|
997
|
+
* — safe to call more than once.
|
|
998
|
+
*
|
|
999
|
+
* One object URL is deliberately kept: the shared zero-byte placeholder
|
|
1000
|
+
* every refused reference resolves to. It costs nothing to leave alive,
|
|
1001
|
+
* and leaving it is what lets this method stay simple — the installed URL
|
|
1002
|
+
* modifier goes on refusing an owned-but-gone reference correctly, rather
|
|
1003
|
+
* than falling back to the original string once the map is empty.
|
|
961
1004
|
*
|
|
962
|
-
* **Does not touch `manager`'s URL modifier
|
|
963
|
-
*
|
|
964
|
-
*
|
|
965
|
-
*
|
|
966
|
-
*
|
|
1005
|
+
* **Does not touch `manager`'s URL modifier.** Resetting it to the
|
|
1006
|
+
* identity function here would reopen the very hole the modifier exists to
|
|
1007
|
+
* close, the moment the same manager was used again — for a second robot,
|
|
1008
|
+
* or for anything else — until a later `prepareUrdfScene` call happened to
|
|
1009
|
+
* overwrite it. The installed modifier is left running,
|
|
967
1010
|
* and with this call's map now empty it already refuses anything it would
|
|
968
1011
|
* have owned and passes through anything it would not have, correctly,
|
|
969
1012
|
* on its own.
|
|
970
1013
|
*/
|
|
971
1014
|
dispose(): void;
|
|
972
1015
|
}
|
|
1016
|
+
/**
|
|
1017
|
+
* A robot's synced files, reachable as `client.assets`: its URDF, the meshes
|
|
1018
|
+
* and textures that URDF references, and the glue a three.js renderer needs
|
|
1019
|
+
* to fetch them with this client's credentials.
|
|
1020
|
+
*/
|
|
973
1021
|
interface AssetsApi {
|
|
974
1022
|
/**
|
|
975
|
-
* Every asset a robot has, plus whether its URDF is complete
|
|
1023
|
+
* Every asset a robot has, plus whether its URDF is complete.
|
|
976
1024
|
* `urdf.missing` names the `package://` URIs the sync could not resolve —
|
|
977
|
-
*
|
|
1025
|
+
* a count alone ("2 meshes missing") sends a developer looking through a
|
|
978
1026
|
* workspace by hand, the URIs are what they can act on.
|
|
979
1027
|
*/
|
|
980
1028
|
list(robotId: string): Promise<AssetListResponse>;
|
|
981
1029
|
/**
|
|
982
1030
|
* The status of one sync by id — for **reconnecting** to a sync already in
|
|
983
|
-
* flight, not for starting one
|
|
1031
|
+
* flight, not for starting one.
|
|
984
1032
|
*
|
|
985
|
-
* **Starting a sync stays out of this SDK, on purpose
|
|
986
|
-
*
|
|
987
|
-
*
|
|
988
|
-
*
|
|
1033
|
+
* **Starting a sync stays out of this SDK, on purpose.** Assets are
|
|
1034
|
+
* transferred only on a developer's explicit request from the console —
|
|
1035
|
+
* an Owner-tier action, done once. This method is a different thing:
|
|
1036
|
+
* `list()`'s
|
|
989
1037
|
* `active_sync` (or a `busy` refusal's `assetSyncBusyDetails`) hands a
|
|
990
1038
|
* caller a `sync_id` for a sync that is **already running**, and before
|
|
991
1039
|
* this method existed there was no way for anything built on this SDK to
|
|
992
|
-
* do anything with that id except throw it away
|
|
993
|
-
*
|
|
994
|
-
* (`GET .../assets/sync/{id}` has existed since W7) — nothing reachable
|
|
995
|
-
* from this SDK ever called it.
|
|
1040
|
+
* do anything with that id except throw it away, even though the sync was
|
|
1041
|
+
* readable server-side the whole time.
|
|
996
1042
|
*
|
|
997
1043
|
* A page reload is the case this exists for: whatever held the `sync_id`
|
|
998
1044
|
* in memory is gone, `list()` (or a fresh `busy` refusal) hands it back,
|
|
@@ -1010,11 +1056,11 @@ interface AssetsApi {
|
|
|
1010
1056
|
*/
|
|
1011
1057
|
urdf(robotId: string): Promise<string>;
|
|
1012
1058
|
/**
|
|
1013
|
-
* The mesh callback for `urdf-loader
|
|
1014
|
-
*
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1017
|
-
*
|
|
1059
|
+
* The mesh callback for `urdf-loader`: an `<img>` tag and the default
|
|
1060
|
+
* three.js loaders cannot set an `Authorization` header, and the platform
|
|
1061
|
+
* deliberately has no signed URLs and no token in the query string, so
|
|
1062
|
+
* every app would otherwise write this glue itself, and each one
|
|
1063
|
+
* differently.
|
|
1018
1064
|
*
|
|
1019
1065
|
* Returns a function with `loadMeshCb`'s own signature — assign it
|
|
1020
1066
|
* directly:
|
|
@@ -1043,7 +1089,7 @@ interface AssetsApi {
|
|
|
1043
1089
|
createMeshLoader(robotId: string, delegate: MeshLoaderDelegate, options?: CreateMeshLoaderOptions): MeshLoaderDelegate;
|
|
1044
1090
|
/**
|
|
1045
1091
|
* Authenticated loading for everything three.js fetches to render a
|
|
1046
|
-
* textured robot — not only meshes
|
|
1092
|
+
* textured robot — not only meshes. Installs
|
|
1047
1093
|
* `manager.setURLModifier` so **every** load `manager` oversees resolves
|
|
1048
1094
|
* to a pre-fetched `blob:` URL: a top-level `<mesh>`, a `<material>`'s
|
|
1049
1095
|
* `<texture>`, and an image a `.dae` references internally via
|
|
@@ -1065,9 +1111,8 @@ interface AssetsApi {
|
|
|
1065
1111
|
* **Do not also install `createMeshLoader` on the same manager.** The two
|
|
1066
1112
|
* consume different URDF sources — this method fetches the URDF's *raw*
|
|
1067
1113
|
* bytes, `createMeshLoader` is meant to pair with `urdf()`'s
|
|
1068
|
-
* cloud-rewritten text. **
|
|
1069
|
-
*
|
|
1070
|
-
* and found it wrong.** What actually happens is asymmetric breakage,
|
|
1114
|
+
* cloud-rewritten text. **It is not that every mesh gets fetched twice.**
|
|
1115
|
+
* What actually happens is asymmetric breakage,
|
|
1071
1116
|
* whichever URDF text the combination ends up parsing: paired with
|
|
1072
1117
|
* *this* method's raw text, `createMeshLoader` receives urdf-loader's
|
|
1073
1118
|
* `resolvePath()` output (`/pkg/rel`) rather than an absolute Fleetless
|
|
@@ -1079,12 +1124,11 @@ interface AssetsApi {
|
|
|
1079
1124
|
* debug the wrong symptom, which is worse than the original (already
|
|
1080
1125
|
* wrong) warning being merely unhelpful.
|
|
1081
1126
|
*
|
|
1082
|
-
* **Enforced, not only documented
|
|
1083
|
-
*
|
|
1084
|
-
*
|
|
1085
|
-
*
|
|
1086
|
-
*
|
|
1087
|
-
* this paragraph. See `managersWithPreparedUrdfScene`.
|
|
1127
|
+
* **Enforced, not only documented.** `createMeshLoader`'s returned
|
|
1128
|
+
* callback checks whether the `manager` it is handed already has this
|
|
1129
|
+
* method's URL modifier installed and fails loudly via
|
|
1130
|
+
* `onComplete(null, err)` before ever touching the network, rather than
|
|
1131
|
+
* relying on a developer having read this paragraph.
|
|
1088
1132
|
*
|
|
1089
1133
|
* **Why raw bytes, not `urdf()`'s rewritten text.** Both `urdf-loader`'s
|
|
1090
1134
|
* default mesh loading and `ColladaLoader` compute the base path they use
|
|
@@ -1099,9 +1143,8 @@ interface AssetsApi {
|
|
|
1099
1143
|
* in sync.
|
|
1100
1144
|
*
|
|
1101
1145
|
* **`urdf-loader` resolves `package://` itself, before any of this runs —
|
|
1102
|
-
* a second resolution stage this method has to account for,
|
|
1103
|
-
*
|
|
1104
|
-
* reading the source.** `URDFLoader.parse()`'s own `resolvePath()`
|
|
1146
|
+
* a second resolution stage this method has to account for, measured in a
|
|
1147
|
+
* real browser rather than read off the source.** `URDFLoader.parse()`'s own `resolvePath()`
|
|
1105
1148
|
* rewrites `package://pkg/rel` using `this.packages` (default `''`) to
|
|
1106
1149
|
* `/pkg/rel` — a root-relative URL — and *that* is what reaches
|
|
1107
1150
|
* `loadMeshCb`/`ColladaLoader`/`manager.resolveURL()`, not the original
|
|
@@ -1121,29 +1164,29 @@ interface AssetsApi {
|
|
|
1121
1164
|
* outside what this method can predict — see the ownership rule below
|
|
1122
1165
|
* for what happens to that reference.
|
|
1123
1166
|
*
|
|
1124
|
-
* **This method only claims what it owns
|
|
1125
|
-
*
|
|
1167
|
+
* **This method only claims what it owns — not every unmapped
|
|
1168
|
+
* reference.** `manager` is frequently the
|
|
1126
1169
|
* caller's own scene-wide `LoadingManager`, shared for an HDRI, an
|
|
1127
1170
|
* environment map, a font atlas, a ground texture — none of which have
|
|
1128
|
-
* anything to do with this robot.
|
|
1129
|
-
*
|
|
1130
|
-
*
|
|
1171
|
+
* anything to do with this robot. Refusing everything unmapped would
|
|
1172
|
+
* silently empty every one of those the moment a caller shares their
|
|
1173
|
+
* manager. So the rule is narrower: a `package://`
|
|
1131
1174
|
* reference, or a root-relative path whose leading segment names a ROS
|
|
1132
1175
|
* package this robot's assets (or `missing`) actually mention, is this
|
|
1133
1176
|
* method's to resolve or refuse; an unmapped one falls back to the
|
|
1134
|
-
* normalized form (
|
|
1177
|
+
* normalized form (below) and then to a shared, inert, page-local
|
|
1135
1178
|
* `blob:` URL — never the original string, so a hostile URDF naming an
|
|
1136
1179
|
* unsynced or off-namespace reference still cannot make three.js touch
|
|
1137
1180
|
* the network for it. Anything else — not in that namespace — is left
|
|
1138
1181
|
* completely alone, **except** an absolute `http(s)` URL, which is
|
|
1139
1182
|
* refused regardless of namespace: the one case this method cannot leave
|
|
1140
1183
|
* ambiguous, because a hostile URDF naming an attacker's host directly
|
|
1141
|
-
* (bypassing `package://` entirely) is exactly what
|
|
1142
|
-
* and three.js would otherwise fetch it for real, off-origin, the moment
|
|
1184
|
+
* (bypassing `package://` entirely) is exactly what this rule exists to
|
|
1185
|
+
* close, and three.js would otherwise fetch it for real, off-origin, the moment
|
|
1143
1186
|
* the direct and namespace checks both miss.
|
|
1144
1187
|
*
|
|
1145
1188
|
* **A `.dae`'s own internal reference gets a second-chance, normalized
|
|
1146
|
-
* lookup
|
|
1189
|
+
* lookup, reproduced in a real browser.** three.js
|
|
1147
1190
|
* builds the request for one by plain string concatenation — no `..`/`.`
|
|
1148
1191
|
* collapsing — while `asset.name` carries the *normalized* tail
|
|
1149
1192
|
* (`@fleetless/contracts`' naming rule). So `../textures/skin.png` or
|
|
@@ -1154,17 +1197,17 @@ interface AssetsApi {
|
|
|
1154
1197
|
* verbatim and `resolvePath()` rewrites it by the same unnormalized
|
|
1155
1198
|
* concatenation on both sides, so the direct key already matches.
|
|
1156
1199
|
*
|
|
1157
|
-
* **`dispose()` does not undo any of this
|
|
1158
|
-
*
|
|
1200
|
+
* **`dispose()` does not undo any of this.** See its own doc comment on
|
|
1201
|
+
* `UrdfSceneResources`.
|
|
1159
1202
|
*
|
|
1160
1203
|
* **Pre-fetch is unavoidable**, not merely a choice: a URL modifier
|
|
1161
1204
|
* cannot be asynchronous, so every asset it might be asked for has to
|
|
1162
1205
|
* already be a `blob:` URL before `URDFLoader.parse` runs. Bounded by
|
|
1163
1206
|
* `options.concurrency` (default 6) and scoped to only `kind: 'mesh'` and
|
|
1164
|
-
* `kind: 'texture'` assets — which is already "what the URDF references"
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
1207
|
+
* `kind: 'texture'` assets — which is already "what the URDF references",
|
|
1208
|
+
* since a re-sync reconciles and assets the current URDF no longer
|
|
1209
|
+
* references stop belonging to the robot. Not an unbounded fetch of
|
|
1210
|
+
* everything the robot has ever had.
|
|
1168
1211
|
*/
|
|
1169
1212
|
prepareUrdfScene(robotId: string, manager: UrdfSceneManager, options?: PrepareUrdfSceneOptions): Promise<UrdfSceneResources>;
|
|
1170
1213
|
}
|
|
@@ -1182,356 +1225,380 @@ type StoredSession = SessionTokens;
|
|
|
1182
1225
|
* assumes a browser, or any storage, exists.
|
|
1183
1226
|
*/
|
|
1184
1227
|
interface TokenStore {
|
|
1228
|
+
/**
|
|
1229
|
+
* Returns the stored session, or `null` when nobody is logged in. May be
|
|
1230
|
+
* async, so a store backed by a native keystore or an IndexedDB read
|
|
1231
|
+
* works without a synchronous cache in front of it.
|
|
1232
|
+
*/
|
|
1185
1233
|
load(): StoredSession | null | Promise<StoredSession | null>;
|
|
1234
|
+
/**
|
|
1235
|
+
* Writes the session, or clears it when passed `null`. Called after a
|
|
1236
|
+
* login, after every silent refresh, and on logout — so an implementation
|
|
1237
|
+
* that persists must expect to be called often, not once.
|
|
1238
|
+
*/
|
|
1186
1239
|
save(session: StoredSession | null): void | Promise<void>;
|
|
1187
1240
|
}
|
|
1188
1241
|
/** The default store: works out of the box, forgets the session on reload. */
|
|
1189
1242
|
declare class InMemoryTokenStore implements TokenStore {
|
|
1190
1243
|
#private;
|
|
1244
|
+
/** Creates an empty store. Nothing is loaded from anywhere — a client built with it starts logged out. */
|
|
1245
|
+
constructor();
|
|
1246
|
+
/** Returns the session held in memory, or `null` if there is none. */
|
|
1191
1247
|
load(): StoredSession | null;
|
|
1248
|
+
/** Replaces the session held in memory; `null` clears it. */
|
|
1192
1249
|
save(session: StoredSession | null): void;
|
|
1193
1250
|
}
|
|
1194
1251
|
|
|
1195
|
-
/**
|
|
1196
|
-
|
|
1252
|
+
/**
|
|
1253
|
+
* `register()`'s input. The app identifier is **not** here: the client already
|
|
1254
|
+
* holds one (`createClient({ appIdentifier })`) and sends it itself, so there
|
|
1255
|
+
* is no way for a caller to register somebody into a different app than the
|
|
1256
|
+
* one this client speaks for.
|
|
1257
|
+
*/
|
|
1258
|
+
interface RegisterOptions {
|
|
1259
|
+
/** The address the verification mail goes to. Nothing works until that link is spent. */
|
|
1260
|
+
email: string;
|
|
1261
|
+
/** At least 12 characters — `clientRegisterRequest` refuses less with a `validation_error`. */
|
|
1262
|
+
password: string;
|
|
1197
1263
|
/**
|
|
1198
|
-
*
|
|
1199
|
-
*
|
|
1200
|
-
*
|
|
1201
|
-
*
|
|
1202
|
-
* not the app identifier").
|
|
1264
|
+
* What the app should call this person. Optional, and **omitted from the
|
|
1265
|
+
* request entirely** when you do not pass it — `clientRegisterRequest` is a
|
|
1266
|
+
* strict schema, so a key carrying `undefined` would be a `422` rather than a
|
|
1267
|
+
* default.
|
|
1203
1268
|
*/
|
|
1204
|
-
|
|
1269
|
+
displayName?: string;
|
|
1270
|
+
}
|
|
1271
|
+
/** `acceptInvitation()`'s input — the token out of the mailed link, plus the password the account gets. */
|
|
1272
|
+
interface AcceptInvitationOptions {
|
|
1273
|
+
/** The `token` from the invitation link the developer's app was linked to. */
|
|
1274
|
+
token: string;
|
|
1275
|
+
/** At least 12 characters. The invitation fixes the role; this call fixes the credential. */
|
|
1276
|
+
password: string;
|
|
1277
|
+
/** Optional, and omitted from the request entirely when absent — same strict-schema reason as `RegisterOptions.displayName`. */
|
|
1278
|
+
displayName?: string;
|
|
1279
|
+
}
|
|
1280
|
+
/** One sign-in button on the app's own login screen, as `listProviders()` lists it. */
|
|
1281
|
+
interface ProviderButton {
|
|
1282
|
+
/** What `beginOidcLogin` addresses this provider by. */
|
|
1283
|
+
slug: string;
|
|
1284
|
+
/** The label the developer configured, to be rendered on the button. */
|
|
1285
|
+
name: string;
|
|
1286
|
+
}
|
|
1287
|
+
/** `beginOidcLogin()`'s input: which provider, and where Fleetless should send the browser back to. */
|
|
1288
|
+
interface BeginOidcLoginOptions {
|
|
1289
|
+
/** A `slug` from `listProviders()`. An unknown one is a `404` when the browser reaches the start route, not here. */
|
|
1290
|
+
slug: string;
|
|
1205
1291
|
/**
|
|
1206
|
-
*
|
|
1207
|
-
*
|
|
1208
|
-
*
|
|
1292
|
+
* Where the browser comes back to with `?code=…&state=…` (or `?error=…`).
|
|
1293
|
+
* Checked against the app's **allowed origins** server-side, by origin — so
|
|
1294
|
+
* the path is yours to choose and the origin is not.
|
|
1295
|
+
*
|
|
1296
|
+
* **Two shapes are refused outright, before the origin is compared**, and
|
|
1297
|
+
* neither refusal mentions them: a URL carrying a **fragment**
|
|
1298
|
+
* (`https://app.example.com/#/auth/callback`) and one carrying **userinfo**
|
|
1299
|
+
* (`https://someone@app.example.com/cb`). Both come back as a flat
|
|
1300
|
+
* `400 invalid_redirect_uri` reading "The redirect_uri is not an origin this
|
|
1301
|
+
* app answers for", which sends people to re-check an allow-list that was
|
|
1302
|
+
* never the problem.
|
|
1303
|
+
*
|
|
1304
|
+
* The fragment case is the one that costs time, because hash routing is the
|
|
1305
|
+
* default for a static-hosted SPA with no server rewrite. Give the callback
|
|
1306
|
+
* a real path (`/auth/callback`) and let your router pick the hash route up
|
|
1307
|
+
* from there; a fragment is a browser-side construct the redirect could not
|
|
1308
|
+
* carry a code in anyway.
|
|
1209
1309
|
*/
|
|
1210
1310
|
redirectUri: string;
|
|
1211
|
-
|
|
1311
|
+
}
|
|
1312
|
+
/** What `beginOidcLogin()` returns. Nothing here has touched the network. */
|
|
1313
|
+
interface OidcLoginRequest {
|
|
1212
1314
|
/**
|
|
1213
|
-
*
|
|
1214
|
-
*
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
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.
|
|
1315
|
+
* Send the end user's browser here. **This SDK does not navigate** — it has
|
|
1316
|
+
* no opinion about whether that is a full page load, a popup or a native web
|
|
1317
|
+
* view, the same boundary `cameras.live` draws by handing back a URL and a
|
|
1318
|
+
* token and stopping there.
|
|
1253
1319
|
*/
|
|
1254
|
-
resource?: string;
|
|
1255
|
-
}
|
|
1256
|
-
/** What `beginHostedLogin()` returns — nothing here has touched the network yet. */
|
|
1257
|
-
interface HostedLoginRequest {
|
|
1258
|
-
/** Send the end user's browser here to start the hosted login page. */
|
|
1259
1320
|
url: string;
|
|
1260
1321
|
/**
|
|
1261
|
-
* Persist this
|
|
1262
|
-
* both back into `
|
|
1263
|
-
*
|
|
1264
|
-
*
|
|
1265
|
-
*
|
|
1266
|
-
*
|
|
1267
|
-
*
|
|
1268
|
-
* use case while looking like it worked for anything that never actually
|
|
1269
|
-
* navigates away. `sessionStorage`, a signed cookie, or a plain variable
|
|
1270
|
-
* (a popup flow that never truly navigates) are all valid — that choice is
|
|
1271
|
-
* the caller's.
|
|
1322
|
+
* Persist this next to `codeVerifier` **before navigating away**, and pass
|
|
1323
|
+
* both back into `completeOidcLogin`. The SDK does not persist them for you:
|
|
1324
|
+
* the redirect back is a fresh page load for a browser app, and nothing kept
|
|
1325
|
+
* in this SDK's memory survives it. `sessionStorage`, a signed cookie or a
|
|
1326
|
+
* plain variable (a popup flow that never truly navigates) are all valid —
|
|
1327
|
+
* that choice is the caller's, and an in-memory default here would look like
|
|
1328
|
+
* it worked right up until the first real redirect.
|
|
1272
1329
|
*/
|
|
1273
1330
|
state: string;
|
|
1331
|
+
/**
|
|
1332
|
+
* The PKCE code verifier for this attempt — persist it exactly as `state`.
|
|
1333
|
+
* **The app runs its own PKCE against Fleetless**, a second exchange
|
|
1334
|
+
* independent of the one Fleetless runs against the identity provider, which
|
|
1335
|
+
* is what makes the one-time code in the redirect worth nothing to whoever
|
|
1336
|
+
* else reads that URL.
|
|
1337
|
+
*/
|
|
1274
1338
|
codeVerifier: string;
|
|
1275
1339
|
}
|
|
1276
|
-
/** `
|
|
1277
|
-
interface
|
|
1278
|
-
/** The `code` query parameter from the redirect back to `redirectUri`. */
|
|
1340
|
+
/** `completeOidcLogin()`'s input — the redirect back, plus what `beginOidcLogin` returned for this same attempt. */
|
|
1341
|
+
interface CompleteOidcLoginOptions {
|
|
1342
|
+
/** The `code` query parameter from the redirect back to `redirectUri`. It lives 60 seconds. */
|
|
1279
1343
|
code: string;
|
|
1280
1344
|
/** The `state` query parameter from that same redirect. */
|
|
1281
1345
|
state: string;
|
|
1282
|
-
/**
|
|
1283
|
-
* The `state` this attempt's `beginHostedLogin` returned. Checked against
|
|
1284
|
-
* `state` above **before any network call** — RFC 6749 §10.12's whole
|
|
1285
|
-
* point is that a caller must not complete an authorization response it
|
|
1286
|
-
* did not itself request.
|
|
1287
|
-
*/
|
|
1346
|
+
/** The `state` this attempt's `beginOidcLogin` returned. Compared **before any network call**. */
|
|
1288
1347
|
expectedState: string;
|
|
1289
|
-
/** The `codeVerifier` this attempt's `
|
|
1348
|
+
/** The `codeVerifier` this attempt's `beginOidcLogin` returned. */
|
|
1290
1349
|
codeVerifier: string;
|
|
1291
|
-
clientId: string;
|
|
1292
|
-
/** Must be the exact same string passed to `beginHostedLogin`. */
|
|
1293
|
-
redirectUri: string;
|
|
1294
|
-
/**
|
|
1295
|
-
* Must be the exact same string passed to `beginHostedLogin`, if any.
|
|
1296
|
-
* Resending it here is not what binds the audience — the server already
|
|
1297
|
-
* bound `resource` to the authorization code at `/oauth/authorize` and
|
|
1298
|
-
* mints `aud` from that stored value regardless of what this call sends —
|
|
1299
|
-
* but RFC 8707 §2 expects a client to name the resource at both steps,
|
|
1300
|
-
* and the cloud rejects a *mismatched* resend outright (`invalid_target`).
|
|
1301
|
-
* Omit it here exactly when it was omitted at `beginHostedLogin`.
|
|
1302
|
-
*/
|
|
1303
|
-
resource?: string;
|
|
1304
1350
|
}
|
|
1305
1351
|
/**
|
|
1306
|
-
* What `
|
|
1307
|
-
*
|
|
1352
|
+
* What `approveMcpInteraction`/`denyMcpInteraction` resolve with: **where to
|
|
1353
|
+
* send the browser**, and nothing else.
|
|
1308
1354
|
*
|
|
1309
|
-
*
|
|
1310
|
-
* that
|
|
1311
|
-
*
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
*
|
|
1319
|
-
*
|
|
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.
|
|
1355
|
+
* A denial carries a redirect too, with `error=access_denied` on it — a client
|
|
1356
|
+
* that is refused has to learn so from its own callback rather than from a page
|
|
1357
|
+
* nobody sent it, so both outcomes end the same way for the app: navigate here.
|
|
1358
|
+
*/
|
|
1359
|
+
interface McpInteractionDecision {
|
|
1360
|
+
/** The absolute URL to navigate to. Wire field `redirect_to`. */
|
|
1361
|
+
redirectTo: string;
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Who the caller is, reachable as `client.auth` — the whole client auth API of
|
|
1365
|
+
* spec §4, as JSON.
|
|
1330
1366
|
*
|
|
1331
|
-
*
|
|
1332
|
-
*
|
|
1333
|
-
*
|
|
1334
|
-
*
|
|
1367
|
+
* **Fleetless serves an app user no page.** The developer's own UI owns every
|
|
1368
|
+
* screen: login, registration, verification, invitation acceptance, password
|
|
1369
|
+
* reset, the provider buttons and the MCP consent. These methods are what those
|
|
1370
|
+
* screens call. The hosted, app-branded login and consent pages this SDK used
|
|
1371
|
+
* to drive are gone, along with `beginHostedLogin`/`completeHostedLogin`.
|
|
1335
1372
|
*
|
|
1336
|
-
*
|
|
1337
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
1342
|
-
*
|
|
1373
|
+
* **The enumeration discipline is the design's, and it shapes this surface.**
|
|
1374
|
+
* `register`, `resendVerification` and `requestPasswordReset` resolve for every
|
|
1375
|
+
* policy-allowed request whether or not the address exists, and `login` answers
|
|
1376
|
+
* the identical `invalid_credentials` for a wrong password, a blocked account
|
|
1377
|
+
* and an unverified one. So: *resolving does not mean an account exists*, and
|
|
1378
|
+
* the only honest refusals are the ones about policy rather than about a
|
|
1379
|
+
* person — `registration_closed`, `domain_not_allowed`, `quota_exceeded`.
|
|
1343
1380
|
*
|
|
1344
|
-
*
|
|
1345
|
-
*
|
|
1346
|
-
* `
|
|
1347
|
-
*
|
|
1381
|
+
* **A client built with a `serverKey` refuses everything that needs an app
|
|
1382
|
+
* user's own session** with `invalid_option`, before any request. `me()`,
|
|
1383
|
+
* `listProviders()`, `mcpInteraction()` and `oidcErrorFromCallback()` still
|
|
1384
|
+
* work on one: the first is what a server key is *for*, the next two are public
|
|
1385
|
+
* reads the cloud answers without any credential at all, and the last touches
|
|
1386
|
+
* no network.
|
|
1348
1387
|
*/
|
|
1349
|
-
interface LogoutResult {
|
|
1350
|
-
revoked: boolean;
|
|
1351
|
-
idp_logout: ClientLogoutResponse['idp_logout'] | null;
|
|
1352
|
-
}
|
|
1353
1388
|
interface AuthApi {
|
|
1354
|
-
/** Exchanges email + password, and the client's configured app identifier, for a session. */
|
|
1355
|
-
login(email: string, password: string): Promise<void>;
|
|
1356
1389
|
/**
|
|
1357
|
-
*
|
|
1358
|
-
*
|
|
1359
|
-
*
|
|
1360
|
-
*
|
|
1361
|
-
* `
|
|
1362
|
-
*
|
|
1363
|
-
*
|
|
1364
|
-
*
|
|
1365
|
-
*
|
|
1366
|
-
*
|
|
1367
|
-
* `
|
|
1368
|
-
*
|
|
1390
|
+
* Self-registration. Writes the account as `pending_verification` and mails
|
|
1391
|
+
* the app's verification link; **the account cannot log in until that link is
|
|
1392
|
+
* spent** (`verifyEmail`).
|
|
1393
|
+
*
|
|
1394
|
+
* Resolves on the route's `202` — which the cloud answers for every
|
|
1395
|
+
* policy-allowed request, whether the address was new or already known. It is
|
|
1396
|
+
* not a claim that an account was created, and an app that renders it as one
|
|
1397
|
+
* ("welcome, Ada!") is showing a stranger the enumeration oracle this whole
|
|
1398
|
+
* family is built to avoid. Render "check your mail" instead.
|
|
1399
|
+
*
|
|
1400
|
+
* Throws a `FleetlessError` carrying the cloud's own code for a refusal, and
|
|
1401
|
+
* that is the distinction this method exists to preserve: `registration_closed`
|
|
1402
|
+
* (the app has self-registration off), `domain_not_allowed` (the address is
|
|
1403
|
+
* outside the app's allowed domains), `quota_exceeded` (the org has as many
|
|
1404
|
+
* app users as its quota allows) and `not_found` (no app carries this
|
|
1405
|
+
* client's `appIdentifier`) are all things the app can say out loud, because
|
|
1406
|
+
* none of them is about whether a person exists.
|
|
1407
|
+
*/
|
|
1408
|
+
register(input: RegisterOptions): Promise<void>;
|
|
1409
|
+
/**
|
|
1410
|
+
* Spends a verification token and **stores the session it answers with**, so
|
|
1411
|
+
* the person is not asked to log in immediately after proving they can read
|
|
1412
|
+
* the mail.
|
|
1413
|
+
*
|
|
1414
|
+
* `token_spent` covers unknown, expired and already-used alike — one code,
|
|
1415
|
+
* because the remedy is one thing: ask for a fresh link with
|
|
1416
|
+
* `resendVerification`. An app rendering this refusal should offer that.
|
|
1369
1417
|
*/
|
|
1370
|
-
|
|
1418
|
+
verifyEmail(token: string): Promise<void>;
|
|
1419
|
+
/** Asks for the verification mail again. Resolves on `202` for every policy-allowed request, existing address or not — same reason as `register`. */
|
|
1420
|
+
resendVerification(email: string): Promise<void>;
|
|
1371
1421
|
/**
|
|
1372
|
-
*
|
|
1373
|
-
*
|
|
1374
|
-
*
|
|
1375
|
-
*
|
|
1376
|
-
*
|
|
1377
|
-
*
|
|
1378
|
-
* `expectedState` throws `no_hosted_login_attempt` — nothing was
|
|
1379
|
-
* persisted for this attempt (a different tab, a restored session,
|
|
1380
|
-
* cleared storage), not necessarily an attack. Only once `expectedState`
|
|
1381
|
-
* is actually present does a mismatch (or a missing `state` on the
|
|
1382
|
-
* callback itself) throw `state_mismatch`. The two are told apart on
|
|
1383
|
-
* purpose: they call for different remedies, and collapsing them would
|
|
1384
|
-
* tell a developer debugging an ordinary storage gap that their app is
|
|
1385
|
-
* under attack.
|
|
1386
|
-
*
|
|
1387
|
-
* Once past that check and the exchange completes, `me()`, `logout()`,
|
|
1388
|
-
* `changePassword()` and silent refresh all behave identically afterwards,
|
|
1389
|
-
* regardless of which flow the session started from. That is the actual
|
|
1390
|
-
* content of §3.4's *"Beide Wege enden im selben Fleetless-Token"*: not
|
|
1391
|
-
* merely that the bytes match, but that every existing code path treats
|
|
1392
|
-
* the result the same way.
|
|
1393
|
-
*
|
|
1394
|
-
* The wire response is RFC 6749 §5.1's envelope (`token_type`, optional
|
|
1395
|
-
* `scope`), not `sessionTokens` — this method normalizes one into the
|
|
1396
|
-
* other before storing. **Refresh needs no separate handling**: the cloud
|
|
1397
|
-
* mints these tokens through the same session mechanism `/api/client/login`
|
|
1398
|
-
* uses (same `refresh_tokens` row, same rotation), so the existing silent
|
|
1399
|
-
* refresh (`/api/client/refresh`) already works for a hosted-login
|
|
1400
|
-
* session — nothing about the origin of a session is tracked or needs to
|
|
1401
|
-
* be.
|
|
1402
|
-
*
|
|
1403
|
-
* Throws with the OAuth error code as `.code` (e.g. `invalid_grant` for an
|
|
1404
|
-
* expired or already-used `code`) if the exchange itself fails — a
|
|
1405
|
-
* different vocabulary from every other method on this interface, because
|
|
1406
|
-
* `/oauth/token` answers in RFC 6749 §5.2's shape, not `apiError`.
|
|
1407
|
-
*
|
|
1408
|
-
* **Makes exactly one request to `/oauth/token` — never retried, no
|
|
1409
|
-
* timeout-and-resend, no internal concurrency of its own.** Stated
|
|
1410
|
-
* because the constraint that matters here is not this method's, it is
|
|
1411
|
-
* the caller's: **never call this a second time for the same `code`
|
|
1412
|
-
* while a first call is still in flight** (a plain "the first attempt
|
|
1413
|
-
* looked like it timed out, so retry" is exactly the shape this warns
|
|
1414
|
-
* against — it is not a defect in this method, since this method itself
|
|
1415
|
-
* has nothing that could ever cause that). André's decision, 2026-08-18:
|
|
1416
|
-
* the platform treats a second presentation of an authorization code as
|
|
1417
|
-
* theft and revokes the whole token family it belongs to, deliberately,
|
|
1418
|
-
* even though a plain double-submission looks identical on the wire —
|
|
1419
|
-
* because the blast radius is bounded (only a caller already holding the
|
|
1420
|
-
* correct `code_verifier` and `client_id` can trigger it, so a merely
|
|
1421
|
-
* *sniffed* code cannot lock anyone out) and the alternative is a
|
|
1422
|
-
* narrower defence against a real theft.
|
|
1423
|
-
*
|
|
1424
|
-
* **What actually happens if two requests race (measured, Argus-W7c):**
|
|
1425
|
-
* one of the two receives `200` with a refresh token that the server has
|
|
1426
|
-
* already revoked. The access token in that same response keeps working
|
|
1427
|
-
* normally for the rest of its short TTL — nothing about the race is
|
|
1428
|
-
* visible yet. The failure surfaces at this session's **first silent
|
|
1429
|
-
* refresh**, as `token_revoked`, potentially many minutes after the race
|
|
1430
|
-
* that actually caused it and with nothing in that later error pointing
|
|
1431
|
-
* back to a retry that "worked". If your own framework, an HTTP client
|
|
1432
|
-
* wrapper, or a user's impatient double-click can cause this method to
|
|
1433
|
-
* be invoked twice concurrently for the same redirect, guard against
|
|
1434
|
-
* that at the call site — a simple in-flight flag or disabling the
|
|
1435
|
-
* triggering control is enough, since there is only ever one legitimate
|
|
1436
|
-
* exchange per authorization code.
|
|
1422
|
+
* Exchanges email + password, and the client's configured app identifier, for
|
|
1423
|
+
* a session.
|
|
1424
|
+
*
|
|
1425
|
+
* `invalid_credentials` is answered identically for a wrong password, a
|
|
1426
|
+
* blocked account and one still waiting to verify. Do not try to tell them
|
|
1427
|
+
* apart — there is nothing in the answer that does, deliberately.
|
|
1437
1428
|
*/
|
|
1438
|
-
|
|
1429
|
+
login(email: string, password: string): Promise<void>;
|
|
1439
1430
|
/**
|
|
1440
|
-
* Ends the session: revokes the whole refresh-token family server-side
|
|
1441
|
-
*
|
|
1442
|
-
*
|
|
1443
|
-
*
|
|
1444
|
-
*
|
|
1445
|
-
*
|
|
1446
|
-
*
|
|
1447
|
-
* family may still be alive server-side even though this client has
|
|
1448
|
-
* forgotten it; an app that cares (a kiosk, a shared workstation) can
|
|
1449
|
-
* warn the user or retry, one that doesn't can ignore it.
|
|
1431
|
+
* Ends the session: revokes the whole refresh-token family server-side (a
|
|
1432
|
+
* stolen refresh token stops working immediately), closes this client's live
|
|
1433
|
+
* realtime connection if it has one, and clears the local store.
|
|
1434
|
+
*
|
|
1435
|
+
* **Never rejects, and always clears the store**, even if the server call
|
|
1436
|
+
* fails: a user who presses "log out" must end up logged out locally
|
|
1437
|
+
* regardless of the network.
|
|
1450
1438
|
*
|
|
1451
1439
|
* **What this does not do:** invalidate the access token already issued.
|
|
1452
|
-
* Access-token checks are stateless (a signed JWT, verified without a
|
|
1453
|
-
*
|
|
1454
|
-
*
|
|
1455
|
-
*
|
|
1456
|
-
*
|
|
1457
|
-
*
|
|
1458
|
-
*
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1461
|
-
*
|
|
1462
|
-
*
|
|
1463
|
-
*
|
|
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).
|
|
1469
|
-
*
|
|
1470
|
-
* **`idp_logout` is `null` when there was no server answer to report** —
|
|
1471
|
-
* and that is *not* the same as `revoked === false`.
|
|
1472
|
-
*
|
|
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.
|
|
1480
|
-
*
|
|
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.
|
|
1486
|
-
*
|
|
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.
|
|
1440
|
+
* Access-token checks are stateless (a signed JWT, verified without a lookup),
|
|
1441
|
+
* so logout has nothing to flip on that token — only on the refresh family
|
|
1442
|
+
* behind it. A token stolen before logout keeps working on REST, and can
|
|
1443
|
+
* still open a *new* realtime connection, until it expires on its own, at
|
|
1444
|
+
* most 15 minutes. That is a deliberate boundary of the stateless-JWT design,
|
|
1445
|
+
* not a bug, but a kiosk or a shared workstation needs to know the number.
|
|
1446
|
+
*
|
|
1447
|
+
* **Nor does it end a session at the identity provider.** It used to report
|
|
1448
|
+
* what was left of one; that apparatus belonged to the hosted login, where
|
|
1449
|
+
* Fleetless owned the browser. The app owns it now, and an app that wants to
|
|
1450
|
+
* end a provider session redirects there itself — knowing its own provider,
|
|
1451
|
+
* which Fleetless never did better than it.
|
|
1491
1452
|
*/
|
|
1492
|
-
logout(): Promise<
|
|
1493
|
-
/** Who the caller turned out to be, without decoding a token client-side. */
|
|
1453
|
+
logout(): Promise<void>;
|
|
1454
|
+
/** Who the caller turned out to be, without decoding a token client-side — which is how apps end up trusting claims nobody verified. */
|
|
1494
1455
|
me(): Promise<ClientIdentity>;
|
|
1495
1456
|
/**
|
|
1496
|
-
* Changes the current
|
|
1497
|
-
*
|
|
1498
|
-
* `currentPassword` is required
|
|
1499
|
-
*
|
|
1500
|
-
*
|
|
1501
|
-
*
|
|
1502
|
-
*
|
|
1503
|
-
*
|
|
1504
|
-
*
|
|
1505
|
-
*
|
|
1506
|
-
*
|
|
1507
|
-
*
|
|
1508
|
-
*
|
|
1509
|
-
* caller holding tokens the server has already revoked, working only
|
|
1510
|
-
* until the access token expires and then silently logged out —
|
|
1511
|
-
* indistinguishable from the change having failed, which is the one
|
|
1512
|
-
* outcome this route exists to prevent. A user with other tabs or
|
|
1513
|
-
* devices logged in will see *those* signed out the moment this
|
|
1514
|
-
* resolves; if your app does not already make that consequence visible
|
|
1515
|
-
* before they confirm, they will find out from a support ticket instead
|
|
1516
|
-
* of from you.
|
|
1457
|
+
* Changes the current app user's password.
|
|
1458
|
+
*
|
|
1459
|
+
* `currentPassword` is required even though the session already proves
|
|
1460
|
+
* identity — it is what stops a stolen *session* from becoming a stolen
|
|
1461
|
+
* *account*.
|
|
1462
|
+
*
|
|
1463
|
+
* **Every other session of this identity is revoked, and this call's own
|
|
1464
|
+
* session is re-issued rather than spared.** The request carries nothing
|
|
1465
|
+
* identifying the caller's own refresh family, so the server revokes all of
|
|
1466
|
+
* them and hands back a fresh pair, which this method stores exactly like
|
|
1467
|
+
* `login`. A user with other tabs or devices signed in will see those signed
|
|
1468
|
+
* out the moment this resolves; if your app does not make that consequence
|
|
1469
|
+
* visible before they confirm, they will find out from a support ticket.
|
|
1517
1470
|
*/
|
|
1518
1471
|
changePassword(currentPassword: string, newPassword: string): Promise<void>;
|
|
1472
|
+
/** Asks for a reset link. Resolves on `202` for a known and an unknown address alike — the answer says nothing about which it was. */
|
|
1473
|
+
requestPasswordReset(email: string): Promise<void>;
|
|
1519
1474
|
/**
|
|
1520
|
-
*
|
|
1521
|
-
*
|
|
1522
|
-
*
|
|
1523
|
-
*
|
|
1524
|
-
*
|
|
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.
|
|
1475
|
+
* Spends a reset token, sets the new password and **stores the session it
|
|
1476
|
+
* answers with**. Every refresh family of that user is revoked first — a
|
|
1477
|
+
* forgotten password is one of the two states where somebody else may be
|
|
1478
|
+
* holding a session.
|
|
1529
1479
|
*/
|
|
1530
|
-
|
|
1480
|
+
confirmPasswordReset(token: string, newPassword: string): Promise<void>;
|
|
1481
|
+
/**
|
|
1482
|
+
* Accepts an app invitation: creates the account (or activates one invited
|
|
1483
|
+
* before it existed) with the role the invitation fixed, and **stores the
|
|
1484
|
+
* session**.
|
|
1485
|
+
*
|
|
1486
|
+
* An invitation always bypasses the app's domain whitelist — a developer
|
|
1487
|
+
* inviting somebody by hand has already made the decision the whitelist
|
|
1488
|
+
* automates.
|
|
1489
|
+
*/
|
|
1490
|
+
acceptInvitation(input: AcceptInvitationOptions): Promise<void>;
|
|
1491
|
+
/**
|
|
1492
|
+
* The app's **enabled** sign-in providers, for drawing the buttons on your
|
|
1493
|
+
* own login screen. A disabled provider is not a button that refuses; it is a
|
|
1494
|
+
* button that is not there.
|
|
1495
|
+
*
|
|
1496
|
+
* Public and unauthenticated, and it carries nothing but `slug` and `name` on
|
|
1497
|
+
* purpose: the issuer, the client id, the scopes and the linking policy are
|
|
1498
|
+
* management-side facts that would tell a stranger how the app's federation
|
|
1499
|
+
* is configured.
|
|
1500
|
+
*/
|
|
1501
|
+
listProviders(): Promise<ProviderButton[]>;
|
|
1502
|
+
/**
|
|
1503
|
+
* Builds the URL that starts a federated sign-in, with a fresh `state` and a
|
|
1504
|
+
* fresh PKCE verifier. **Makes no network call and does not navigate** —
|
|
1505
|
+
* persist `state` and `codeVerifier`, then send the browser to `url`.
|
|
1506
|
+
*
|
|
1507
|
+
* Nothing about the app, the provider or the redirect URI is validated here;
|
|
1508
|
+
* it is all checked when the browser actually reaches the route, in that
|
|
1509
|
+
* order, with the redirect target checked before the provider so that a
|
|
1510
|
+
* caller who got the target wrong learns nothing about which providers the
|
|
1511
|
+
* app has.
|
|
1512
|
+
*
|
|
1513
|
+
* Async only because the S256 `code_challenge` needs `crypto.subtle.digest`,
|
|
1514
|
+
* which the Web Crypto API only ever offers as a promise.
|
|
1515
|
+
*/
|
|
1516
|
+
beginOidcLogin(input: BeginOidcLoginOptions): Promise<OidcLoginRequest>;
|
|
1517
|
+
/**
|
|
1518
|
+
* Completes a federated sign-in: checks `state` against `expectedState`,
|
|
1519
|
+
* trades the one-time `code` for a session, and stores it.
|
|
1520
|
+
*
|
|
1521
|
+
* **The state check runs before any request is sent.** RFC 6749 §10.12's
|
|
1522
|
+
* whole point is that a client must not complete an authorization response it
|
|
1523
|
+
* did not itself request — a check made after the exchange would already have
|
|
1524
|
+
* spent a code for a flow this client never started. Both an outright
|
|
1525
|
+
* mismatch and an *empty* `expectedState` throw `state_mismatch`; the message
|
|
1526
|
+
* says which, because the remedies differ ("check how your app persisted the
|
|
1527
|
+
* value" versus "this response belongs to a sign-in you did not start") even
|
|
1528
|
+
* though the next step is the same either way — start the sign-in again.
|
|
1529
|
+
*
|
|
1530
|
+
* The code lives 60 seconds and is single-use. Unknown, expired, replayed and
|
|
1531
|
+
* "the account was blocked in between" all arrive as one `token_spent`,
|
|
1532
|
+
* because the app has nothing different to do about any of them.
|
|
1533
|
+
*/
|
|
1534
|
+
completeOidcLogin(input: CompleteOidcLoginOptions): Promise<void>;
|
|
1535
|
+
/**
|
|
1536
|
+
* Reads a **failed** federated sign-in off the redirect back, as a
|
|
1537
|
+
* `FleetlessError` you can branch on, or `null` when the callback carries no
|
|
1538
|
+
* `error` at all.
|
|
1539
|
+
*
|
|
1540
|
+
* Fleetless renders no page for these: the reason is carried to your own
|
|
1541
|
+
* `redirectUri` as `?error=<code>`, and this turns that string into the same
|
|
1542
|
+
* error type every other method throws. A code the contracts define (see
|
|
1543
|
+
* `ClientOidcErrorCode`) becomes that code verbatim; anything else becomes
|
|
1544
|
+
* `unexpected_response` with the raw value in the message, rather than being
|
|
1545
|
+
* passed through as a code neither side defines.
|
|
1546
|
+
*
|
|
1547
|
+
* Purely local — it parses a query string and asks nothing.
|
|
1548
|
+
*/
|
|
1549
|
+
oidcErrorFromCallback(params: URLSearchParams): FleetlessError | null;
|
|
1550
|
+
/**
|
|
1551
|
+
* Reads a pending MCP authorization by the interaction id the browser
|
|
1552
|
+
* arrived with, so the app can render its own consent screen.
|
|
1553
|
+
*
|
|
1554
|
+
* **`client_name` is a string the client typed about itself** during an
|
|
1555
|
+
* unauthenticated dynamic registration — nobody checked it, which is why
|
|
1556
|
+
* `client_name_verified` is the literal `false` rather than a boolean with a
|
|
1557
|
+
* `true` branch that could never happen. Do not render it as an identity.
|
|
1558
|
+
*
|
|
1559
|
+
* `interaction_expired` means exactly that: ten minutes ran out, or the id
|
|
1560
|
+
* was never real. Both answer the same way, so the screen to show is "that
|
|
1561
|
+
* took too long, start again" rather than an error.
|
|
1562
|
+
*
|
|
1563
|
+
* **Call this with the app user already signed in.** The route needs no
|
|
1564
|
+
* credential, but it reads one if present, and `already_granted` is `false`
|
|
1565
|
+
* for an anonymous read whatever the truth is — so a consent screen rendered
|
|
1566
|
+
* from an unauthenticated call asks a person to agree to something they
|
|
1567
|
+
* agreed to already. An **expired** token counts as anonymous to this route,
|
|
1568
|
+
* which answers `200` rather than refusing, so this method probes the
|
|
1569
|
+
* session's liveness first and refreshes if it can; a session that cannot be
|
|
1570
|
+
* refreshed is not an error here, it is genuinely anonymous.
|
|
1571
|
+
*/
|
|
1572
|
+
mcpInteraction(id: string): Promise<ClientMcpInteraction>;
|
|
1573
|
+
/** Approves a pending MCP authorization on behalf of the signed-in app user, and returns where to send the browser. */
|
|
1574
|
+
approveMcpInteraction(id: string): Promise<McpInteractionDecision>;
|
|
1575
|
+
/** Denies one. Also returns a redirect — with `error=access_denied` on it, so the client learns from its own callback. */
|
|
1576
|
+
denyMcpInteraction(id: string): Promise<McpInteractionDecision>;
|
|
1577
|
+
/**
|
|
1578
|
+
* Every MCP client this app user has standing consent for — the "connected
|
|
1579
|
+
* apps" list, and the door out of a decision a person could otherwise make
|
|
1580
|
+
* once and never unmake. A withdrawn grant is never listed.
|
|
1581
|
+
*
|
|
1582
|
+
* `client_name_verified` is `false` here for the reason it is on
|
|
1583
|
+
* `mcpInteraction`, and it matters more rather than less: a list like this is
|
|
1584
|
+
* read long after the moment of approval, when nobody remembers what they
|
|
1585
|
+
* clicked.
|
|
1586
|
+
*/
|
|
1587
|
+
listMcpGrants(): Promise<McpConsentGrant[]>;
|
|
1588
|
+
/**
|
|
1589
|
+
* Withdraws one standing consent by the client's id.
|
|
1590
|
+
*
|
|
1591
|
+
* Resolves whether or not there was anything to withdraw — a client id this
|
|
1592
|
+
* account never approved and one it withdrew a minute ago both land on the
|
|
1593
|
+
* end state the caller asked for. A refusal there would tell a caller which
|
|
1594
|
+
* clients an account has connected, and would turn a double-clicked button
|
|
1595
|
+
* into a failure.
|
|
1596
|
+
*/
|
|
1597
|
+
revokeMcpGrant(clientId: string): Promise<void>;
|
|
1531
1598
|
}
|
|
1532
1599
|
|
|
1533
1600
|
/**
|
|
1534
|
-
* The snapshot's metadata alone, without the bytes
|
|
1601
|
+
* The snapshot's metadata alone, without the bytes. All fields
|
|
1535
1602
|
* are `null` together when nothing has been captured yet for this camera —
|
|
1536
1603
|
* a fresh configuration before the first `snapshot_interval_ms` elapses, say
|
|
1537
1604
|
* — which is a state, not a failure: the wire answers it with
|
|
@@ -1546,36 +1613,49 @@ interface AuthApi {
|
|
|
1546
1613
|
* `SNAPSHOT_HEADERS`'s doc comment in `@fleetless/contracts`).
|
|
1547
1614
|
*/
|
|
1548
1615
|
interface CameraSnapshotMeta {
|
|
1616
|
+
/** The image's media type, e.g. `image/jpeg`. */
|
|
1549
1617
|
mime: string | null;
|
|
1618
|
+
/** The image's width in pixels. */
|
|
1550
1619
|
width: number | null;
|
|
1620
|
+
/** The image's height in pixels. */
|
|
1551
1621
|
height: number | null;
|
|
1552
|
-
/** The bridge's capture time
|
|
1622
|
+
/** The bridge's capture time, in unix milliseconds — when the frame was taken, not when it was served. */
|
|
1553
1623
|
timestamp_ms: number | null;
|
|
1624
|
+
/** How long the cloud has held this frame, in milliseconds. The one figure to read for freshness. */
|
|
1554
1625
|
age_ms: number | null;
|
|
1555
1626
|
}
|
|
1556
|
-
/** One snapshot read: the image bytes plus everything needed to state how old they are
|
|
1627
|
+
/** One snapshot read: the image bytes plus everything needed to state how old they are. */
|
|
1557
1628
|
interface CameraSnapshot extends CameraSnapshotMeta {
|
|
1629
|
+
/** The encoded image, or `null` when nothing has been captured yet. */
|
|
1558
1630
|
image: Uint8Array | null;
|
|
1559
1631
|
}
|
|
1560
1632
|
/**
|
|
1561
|
-
* What a LiveKit client needs to join, plus the means to leave
|
|
1562
|
-
*
|
|
1563
|
-
*
|
|
1564
|
-
*
|
|
1565
|
-
*
|
|
1633
|
+
* What a LiveKit client needs to join, plus the means to leave. Returned by
|
|
1634
|
+
* `cameras.live`. Hand `url` and `token` straight to a LiveKit client SDK
|
|
1635
|
+
* (e.g. `Room.connect(url, token)`) — this SDK stops there on purpose: no
|
|
1636
|
+
* video widget and no teleop-style helper, so that the app owns how the
|
|
1637
|
+
* video is presented.
|
|
1566
1638
|
*/
|
|
1567
1639
|
interface CameraLiveSession {
|
|
1568
1640
|
/**
|
|
1569
|
-
* This viewer's own hold
|
|
1641
|
+
* This viewer's own hold — what `release()` releases, and the only
|
|
1570
1642
|
* thing distinguishing this session from every other tab of the same
|
|
1571
|
-
* identity watching the same camera.
|
|
1572
|
-
* with no id released **all** of this identity's holds on the slug, so one
|
|
1573
|
-
* tab closing stopped the robot for every other tab too. See `release()`'s
|
|
1574
|
-
* doc comment for what changed and what did not.
|
|
1643
|
+
* identity watching the same camera.
|
|
1575
1644
|
*/
|
|
1576
1645
|
session_id: string;
|
|
1646
|
+
/** The LiveKit server URL to connect to. */
|
|
1577
1647
|
url: string;
|
|
1648
|
+
/** The LiveKit room this session joins. */
|
|
1578
1649
|
room: string;
|
|
1650
|
+
/**
|
|
1651
|
+
* The LiveKit access token for this session. It is checked when the
|
|
1652
|
+
* participant connects and not again afterwards, so it bounds *joining*,
|
|
1653
|
+
* not the session: a viewer who has already joined keeps receiving video
|
|
1654
|
+
* past `expires_at`. What ends a joined session is `release()` together
|
|
1655
|
+
* with disconnecting the room, the cloud reconciling the hold away
|
|
1656
|
+
* against LiveKit's real participants, or a revocation — a membership,
|
|
1657
|
+
* role or key change — kicking the participant out.
|
|
1658
|
+
*/
|
|
1579
1659
|
token: string;
|
|
1580
1660
|
/**
|
|
1581
1661
|
* When this token can no longer be used to **join** — not when an
|
|
@@ -1594,14 +1674,10 @@ interface CameraLiveSession {
|
|
|
1594
1674
|
expires_at: string;
|
|
1595
1675
|
/**
|
|
1596
1676
|
* Tells the cloud this viewer no longer wants to hold the camera live —
|
|
1597
|
-
* **this** hold, addressed by `session_id
|
|
1598
|
-
*
|
|
1599
|
-
*
|
|
1600
|
-
*
|
|
1601
|
-
* stopped the robot out from under every other tab of the same logged-in
|
|
1602
|
-
* user, which kept rendering a frozen frame because a LiveKit token is
|
|
1603
|
-
* checked at join and never again. Each `CameraLiveSession` now releases
|
|
1604
|
-
* only the hold it itself took.
|
|
1677
|
+
* **this** hold, addressed by `session_id`, and no other tab's. Each
|
|
1678
|
+
* `CameraLiveSession` releases only the hold it itself took, so one tab's
|
|
1679
|
+
* cleanup never stops the robot out from under another tab of the same
|
|
1680
|
+
* logged-in user.
|
|
1605
1681
|
*
|
|
1606
1682
|
* **This alone does not stop the stream.** The cloud makes LiveKit room
|
|
1607
1683
|
* participation the authoritative refcount, not this call — precisely
|
|
@@ -1622,7 +1698,7 @@ interface CameraLiveSession {
|
|
|
1622
1698
|
*
|
|
1623
1699
|
* **A failed DELETE here is not observable anywhere** — not as a
|
|
1624
1700
|
* rejection, not as a realtime event, not as a field on this object. This
|
|
1625
|
-
* is a deliberate decision, not an oversight
|
|
1701
|
+
* is a deliberate decision, not an oversight: the only
|
|
1626
1702
|
* consumer of that information would be code deciding whether to retry,
|
|
1627
1703
|
* and the backstop this comment already describes — the cloud's own
|
|
1628
1704
|
* LiveKit-participation reconciliation — makes a retry unnecessary for
|
|
@@ -1632,13 +1708,18 @@ interface CameraLiveSession {
|
|
|
1632
1708
|
*/
|
|
1633
1709
|
release(): Promise<void>;
|
|
1634
1710
|
}
|
|
1711
|
+
/**
|
|
1712
|
+
* A robot's cameras, reachable as `client.cameras`: what exists, the latest
|
|
1713
|
+
* still frame, and a live video session. All of it is REST — no realtime
|
|
1714
|
+
* channel is involved.
|
|
1715
|
+
*/
|
|
1635
1716
|
interface CamerasApi {
|
|
1636
|
-
/** Every camera exposed on this robot
|
|
1717
|
+
/** Every camera exposed on this robot, as descriptors — the same per-robot list the other kinds use. */
|
|
1637
1718
|
list(robotId: string): Promise<CameraDescriptor[]>;
|
|
1638
1719
|
/**
|
|
1639
1720
|
* The current snapshot: image bytes plus its age. Independent of `live` —
|
|
1640
1721
|
* a snapshot keeps updating on `snapshot_interval_ms` whether or not
|
|
1641
|
-
* anyone is watching live
|
|
1722
|
+
* anyone is watching live, and keeps being served, with a growing
|
|
1642
1723
|
* age, even while the bridge is offline.
|
|
1643
1724
|
*/
|
|
1644
1725
|
snapshot(robotId: string, slug: string): Promise<CameraSnapshot>;
|
|
@@ -1650,30 +1731,44 @@ interface CamerasApi {
|
|
|
1650
1731
|
*/
|
|
1651
1732
|
snapshotMeta(robotId: string, slug: string): Promise<CameraSnapshotMeta>;
|
|
1652
1733
|
/**
|
|
1653
|
-
* Takes a refcounted hold on this camera's live stream
|
|
1734
|
+
* Takes a refcounted hold on this camera's live stream: the
|
|
1654
1735
|
* first `live()` on a slug starts the robot publishing, the last viewer
|
|
1655
1736
|
* leaving stops it. Deliberately not deduplicated locally across multiple
|
|
1656
1737
|
* `live()` calls for the same `(robotId, slug)` — unlike a datapoint
|
|
1657
1738
|
* subscription, each call needs its own distinct LiveKit participant, so
|
|
1658
|
-
* a local counter here would just be the same shared-count bug
|
|
1659
|
-
*
|
|
1660
|
-
*
|
|
1739
|
+
* a local counter here would just be the same shared-count bug the
|
|
1740
|
+
* subscription layer already fixed, self-inflicted on a resource the
|
|
1741
|
+
* cloud counts correctly on its own.
|
|
1661
1742
|
*/
|
|
1662
1743
|
live(robotId: string, slug: string): Promise<CameraLiveSession>;
|
|
1663
1744
|
}
|
|
1664
1745
|
|
|
1746
|
+
/**
|
|
1747
|
+
* The callbacks `datapoints.subscribe` reports through: one for values, one
|
|
1748
|
+
* for a refusal. `onEvent` fires immediately with the current value and
|
|
1749
|
+
* again on every change.
|
|
1750
|
+
*/
|
|
1665
1751
|
interface DatapointSubscriptionHandlers {
|
|
1666
1752
|
/** Called with the current value on subscribe, then again on every change. */
|
|
1667
1753
|
onEvent(event: DatapointEvent): void;
|
|
1668
|
-
/** Called once if the subscription is refused
|
|
1754
|
+
/** Called once if the subscription is refused, e.g. `forbidden` or `unknown_datapoint`. */
|
|
1669
1755
|
onError?(error: FleetlessError): void;
|
|
1670
1756
|
}
|
|
1757
|
+
/** A live datapoint subscription, returned by `datapoints.subscribe`. */
|
|
1671
1758
|
interface DatapointSubscription {
|
|
1672
|
-
/**
|
|
1759
|
+
/**
|
|
1760
|
+
* Stops this subscription. The `unsubscribe` frame reaches the server only
|
|
1761
|
+
* when this was the **last** holder of the robot and slug pair, and only if
|
|
1762
|
+
* the channel is connected — subscriptions are reference-counted across
|
|
1763
|
+
* kinds, so releasing this one while a second `datapoints.subscribe`, an
|
|
1764
|
+
* `actions.subscribe` or an in-flight `services.call` still holds the same
|
|
1765
|
+
* pair leaves that one's stream running untouched. Safe to call more than
|
|
1766
|
+
* once.
|
|
1767
|
+
*/
|
|
1673
1768
|
unsubscribe(): void;
|
|
1674
1769
|
}
|
|
1675
1770
|
/**
|
|
1676
|
-
* Window aggregation for `history
|
|
1771
|
+
* Window aggregation for `datapoints.history`. `window` and `agg` always
|
|
1677
1772
|
* travel together on the wire — the cloud refuses one without the other
|
|
1678
1773
|
* rather than defaulting either, since a silently chosen aggregation is a
|
|
1679
1774
|
* chart that lies quietly — so they live in one object here instead of two
|
|
@@ -1685,10 +1780,16 @@ interface DatapointSubscription {
|
|
|
1685
1780
|
interface HistoryAggregation {
|
|
1686
1781
|
/** Bucket width, e.g. `10s`, `1m`. */
|
|
1687
1782
|
window: string;
|
|
1783
|
+
/** How to reduce each bucket's samples to one number. */
|
|
1688
1784
|
agg: 'min' | 'max' | 'avg';
|
|
1689
|
-
/** A numeric field inside an object value, e.g. `pose.x
|
|
1785
|
+
/** A numeric field inside an object value, e.g. `pose.x`. Only meaningful when the datapoint's own value is not itself a number. */
|
|
1690
1786
|
field?: string;
|
|
1691
1787
|
}
|
|
1788
|
+
/**
|
|
1789
|
+
* The window `datapoints.history` reads, and whether it comes back as raw
|
|
1790
|
+
* samples or as aggregated buckets. `aggregate` is what decides which of
|
|
1791
|
+
* the two responses you get.
|
|
1792
|
+
*/
|
|
1692
1793
|
interface HistoryOptions {
|
|
1693
1794
|
/**
|
|
1694
1795
|
* `now-30s` / `now-5m` / `now-1h`, or absolute unix milliseconds — as a
|
|
@@ -1701,11 +1802,22 @@ interface HistoryOptions {
|
|
|
1701
1802
|
from: string;
|
|
1702
1803
|
/** Same two forms as `from`. Defaults to now. */
|
|
1703
1804
|
to?: string;
|
|
1805
|
+
/** The most rows to return. The platform applies its own ceiling regardless. */
|
|
1704
1806
|
limit?: number;
|
|
1705
1807
|
/** Present: the result is aggregated buckets. Absent: raw samples. */
|
|
1706
1808
|
aggregate?: HistoryAggregation;
|
|
1707
1809
|
}
|
|
1810
|
+
/**
|
|
1811
|
+
* A robot's exposed values, reachable as `client.datapoints`: the latest
|
|
1812
|
+
* one, a live subscription to it, and — for a datapoint configured with
|
|
1813
|
+
* retention — its recorded history.
|
|
1814
|
+
*/
|
|
1708
1815
|
interface DatapointsApi {
|
|
1816
|
+
/**
|
|
1817
|
+
* Reads the datapoint's latest value over REST, once. It carries the
|
|
1818
|
+
* bridge's own capture time, so a caller can tell a fresh value from a
|
|
1819
|
+
* stale one without a subscription.
|
|
1820
|
+
*/
|
|
1709
1821
|
get(robotId: string, slug: string): Promise<DatapointValue>;
|
|
1710
1822
|
/**
|
|
1711
1823
|
* Subscribes over the realtime channel. Reconnect and re-authentication
|
|
@@ -1718,25 +1830,27 @@ interface DatapointsApi {
|
|
|
1718
1830
|
* other — the `unsubscribe` frame is sent only when the last subscriber
|
|
1719
1831
|
* on that pair goes away. This matters in practice: two widgets showing
|
|
1720
1832
|
* the same battery value, or a component mounted twice under React
|
|
1721
|
-
* StrictMode, both subscribe to the same key.
|
|
1722
|
-
* `
|
|
1723
|
-
*
|
|
1833
|
+
* StrictMode, both subscribe to the same key. That count is shared with
|
|
1834
|
+
* `actions.subscribe` and `services.call` — a slug is one namespace across
|
|
1835
|
+
* kinds, and so is its subscription.
|
|
1724
1836
|
*/
|
|
1725
1837
|
subscribe(robotId: string, slug: string, handlers: DatapointSubscriptionHandlers): DatapointSubscription;
|
|
1726
1838
|
/**
|
|
1727
|
-
* Reads recorded history for a `retention: true` datapoint
|
|
1728
|
-
*
|
|
1729
|
-
*
|
|
1730
|
-
*
|
|
1731
|
-
*
|
|
1732
|
-
*
|
|
1733
|
-
*
|
|
1734
|
-
*
|
|
1735
|
-
*
|
|
1839
|
+
* Reads recorded history for a `retention: true` datapoint over REST — no
|
|
1840
|
+
* realtime channel involved, the same way `cameras.snapshot` isn't. **This
|
|
1841
|
+
* overload is the aggregated one:** `aggregate` is given, so it resolves
|
|
1842
|
+
* with `HistoryBucketsResponse` (`kind: 'buckets'`) — one row per window,
|
|
1843
|
+
* reduced by `aggregate.agg`. Leave `aggregate` out and the other overload
|
|
1844
|
+
* gives you raw samples instead.
|
|
1845
|
+
*
|
|
1846
|
+
* Two overloads rather than one union so a caller who already knows which
|
|
1847
|
+
* they asked for isn't forced to narrow something they determined
|
|
1848
|
+
* themselves. `kind` still carries the same information on both, so code
|
|
1849
|
+
* holding the result dynamically can still branch on it.
|
|
1736
1850
|
*
|
|
1737
1851
|
* **Rejects, does not silently empty out, two specific refusals** —
|
|
1738
1852
|
* unlike `cameras.snapshot`'s absorption of `no_snapshot_yet` into a null
|
|
1739
|
-
* read, these two must reach the caller as
|
|
1853
|
+
* read, these two must reach the caller as a rejected `FleetlessError`:
|
|
1740
1854
|
* - `not_recorded` — the slug exists and is granted, but is configured
|
|
1741
1855
|
* live-only. An empty result here would look exactly like "recorded,
|
|
1742
1856
|
* but nothing in this window", and the two need opposite fixes: turn
|
|
@@ -1747,75 +1861,40 @@ interface DatapointsApi {
|
|
|
1747
1861
|
history(robotId: string, slug: string, options: HistoryOptions & {
|
|
1748
1862
|
aggregate: HistoryAggregation;
|
|
1749
1863
|
}): Promise<HistoryBucketsResponse>;
|
|
1864
|
+
/**
|
|
1865
|
+
* The same read without `aggregate`: resolves with
|
|
1866
|
+
* `HistorySamplesResponse` (`kind: 'samples'`), every recorded sample in
|
|
1867
|
+
* the window as a `timestamp_ms` and a `value`. `timestamp_ms` is the
|
|
1868
|
+
* bridge's own capture time, the same instant the live value carried, so a
|
|
1869
|
+
* recorded point and a live one sit on one axis without apology.
|
|
1870
|
+
*
|
|
1871
|
+
* **Read `truncated`.** The platform caps how much one read returns, by
|
|
1872
|
+
* row count or by bytes, and `truncated_by` says which. A short array that
|
|
1873
|
+
* does not admit it is indistinguishable from a quiet period, and the two
|
|
1874
|
+
* lead to opposite conclusions. Refuses `not_recorded` the same way the
|
|
1875
|
+
* aggregated overload does.
|
|
1876
|
+
*/
|
|
1750
1877
|
history(robotId: string, slug: string, options: HistoryOptions & {
|
|
1751
1878
|
aggregate?: undefined;
|
|
1752
1879
|
}): Promise<HistorySamplesResponse>;
|
|
1753
1880
|
}
|
|
1754
1881
|
|
|
1755
1882
|
/**
|
|
1756
|
-
*
|
|
1757
|
-
*
|
|
1758
|
-
*
|
|
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.
|
|
1883
|
+
* Robot-wide job reads, reachable as `client.jobs`. Everything here is
|
|
1884
|
+
* addressed by robot rather than by slug, which is what `actions` and
|
|
1885
|
+
* `services` cannot do.
|
|
1764
1886
|
*/
|
|
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
|
-
|
|
1807
1887
|
interface JobsApi {
|
|
1808
1888
|
/**
|
|
1809
|
-
* Every job the platform currently believes this robot has
|
|
1810
|
-
* /api/robots/:id/jobs` (W6b, contracts `robotJobsResponse` doc comment).
|
|
1889
|
+
* Every job the platform currently believes this robot has.
|
|
1811
1890
|
*
|
|
1812
1891
|
* `actions.subscribe`/`services.call` and `GET /jobs/:slug` (the per-slug
|
|
1813
1892
|
* route those build on) all require already knowing the slug. That is not
|
|
1814
1893
|
* always true: a reconnecting bridge can name a job the cloud only
|
|
1815
1894
|
* *adopted*, and a configuration change can leave a job on a slug the
|
|
1816
1895
|
* published document no longer contains. Both are jobs no slug can name,
|
|
1817
|
-
* which is exactly what this method is for
|
|
1818
|
-
*
|
|
1896
|
+
* which is exactly what this method is for — an app developer has no
|
|
1897
|
+
* other way to reach them.
|
|
1819
1898
|
*
|
|
1820
1899
|
* At most one entry per slug: the current job there, exactly what a
|
|
1821
1900
|
* per-slug read would answer for that slug. Not a history endpoint.
|
|
@@ -1832,18 +1911,22 @@ interface JobsApi {
|
|
|
1832
1911
|
* `hello`, having never minted it, and has no other honest value to put
|
|
1833
1912
|
* there. So this is newest-*known*-first: a job the robot has been
|
|
1834
1913
|
* running for an hour can sit above one started a minute ago, if the
|
|
1835
|
-
* hour-long one was only just adopted
|
|
1836
|
-
* comment).
|
|
1914
|
+
* hour-long one was only just adopted.
|
|
1837
1915
|
*/
|
|
1838
1916
|
list(robotId: string): Promise<Job[]>;
|
|
1839
1917
|
}
|
|
1840
1918
|
|
|
1919
|
+
/**
|
|
1920
|
+
* One-way messages to a robot's publishers, reachable as
|
|
1921
|
+
* `client.publishers` — a velocity command, a goal pose, anything the
|
|
1922
|
+
* developer exposed as a publisher.
|
|
1923
|
+
*/
|
|
1841
1924
|
interface PublishersApi {
|
|
1842
1925
|
/**
|
|
1843
|
-
* Publishes one message to a publisher
|
|
1926
|
+
* Publishes one message to a publisher.
|
|
1844
1927
|
*
|
|
1845
1928
|
* This is a plain method call — there is deliberately no deadman switch,
|
|
1846
|
-
* rate governor or "takt" helper here
|
|
1929
|
+
* rate governor or "takt" helper here. The bridge's own
|
|
1847
1930
|
* `timeout_ms` failsafe is the platform's safety primitive: if messages
|
|
1848
1931
|
* stop arriving — including because this process crashed — the bridge
|
|
1849
1932
|
* publishes the configured failsafe message itself. That does **not**
|
|
@@ -1854,45 +1937,54 @@ interface PublishersApi {
|
|
|
1854
1937
|
* building a publisher-driven control loop.
|
|
1855
1938
|
*
|
|
1856
1939
|
* Rejects `publisher_busy` while a different user is publishing and has
|
|
1857
|
-
* not been quiet for
|
|
1940
|
+
* not been quiet for its configured quiet timeout yet — whoever publishes
|
|
1858
1941
|
* holds the publisher implicitly exclusive.
|
|
1859
1942
|
*/
|
|
1860
1943
|
publish(robotId: string, slug: string, message: Record<string, unknown>, options?: SendCommandOptions): Promise<void>;
|
|
1861
1944
|
}
|
|
1862
1945
|
|
|
1946
|
+
/**
|
|
1947
|
+
* Request/response calls to a robot, reachable as `client.services`. A
|
|
1948
|
+
* service answers once and is done, which is why this namespace has a
|
|
1949
|
+
* single method and nothing to subscribe to.
|
|
1950
|
+
*/
|
|
1863
1951
|
interface ServicesApi {
|
|
1864
1952
|
/**
|
|
1865
|
-
* Calls a service and resolves with its result
|
|
1866
|
-
*
|
|
1867
|
-
*
|
|
1868
|
-
* call") — but that is deliberately invisible here: the caller gets a
|
|
1953
|
+
* Calls a service and resolves with its result. A service call is a job
|
|
1954
|
+
* underneath — the same `job_id` exchange and disconnect survival as an
|
|
1955
|
+
* action — but that is deliberately invisible here: the caller gets a
|
|
1869
1956
|
* plain `Promise<result>`, matching the REST `serviceCallResponse` shape's
|
|
1870
1957
|
* developer experience. There is nothing to subscribe to for a service —
|
|
1871
1958
|
* no feedback, no progress, no cancel — so this call already waits for
|
|
1872
1959
|
* the terminal state internally.
|
|
1873
1960
|
*
|
|
1874
|
-
* `options.patienceMs` bounds the **whole wait** for a service call
|
|
1875
|
-
*
|
|
1961
|
+
* `options.patienceMs` bounds the **whole wait** for a service call —
|
|
1962
|
+
* unlike an action, where it bounds acceptance only — because a service
|
|
1876
1963
|
* has no further state to observe once it settles; the platform gives up
|
|
1877
1964
|
* on the ROS call itself after this long.
|
|
1878
1965
|
*
|
|
1879
1966
|
* `options.timeoutMs` bounds this SDK's own local wait for the WHOLE
|
|
1880
1967
|
* call — the ack that a job was created, plus however much of the
|
|
1881
|
-
* budget is left for it to then reach a terminal state
|
|
1968
|
+
* budget is left for it to then reach a terminal state — not two
|
|
1882
1969
|
* separate `timeoutMs`-length windows back to back. A caller who sets
|
|
1883
1970
|
* `timeoutMs: 5000` is bounding total latency at ~5s, not ~10s; the
|
|
1884
1971
|
* number means what it says, once, for the whole call.
|
|
1885
1972
|
*
|
|
1886
|
-
* It is also **not independent** of `patienceMs
|
|
1973
|
+
* It is also **not independent** of `patienceMs`: left unset, it
|
|
1887
1974
|
* is derived from `patienceMs` so this SDK's local clock cannot fire
|
|
1888
1975
|
* before the platform's own deadline has even been reached. Setting
|
|
1889
|
-
* both, with `timeoutMs` shorter than `patienceMs`,
|
|
1890
|
-
* `invalid_option`
|
|
1891
|
-
*
|
|
1976
|
+
* both, with `timeoutMs` shorter than `patienceMs`, rejects with
|
|
1977
|
+
* `invalid_option` before any request is sent rather than letting the two
|
|
1978
|
+
* race — see `InvokeOptions.patienceMs` for the full reasoning.
|
|
1892
1979
|
*/
|
|
1893
1980
|
call(robotId: string, slug: string, params: Record<string, unknown>, options?: InvokeOptions): Promise<unknown>;
|
|
1894
1981
|
}
|
|
1895
1982
|
|
|
1983
|
+
/**
|
|
1984
|
+
* Everything `createClient` accepts. `apiUrl` and `appIdentifier` are
|
|
1985
|
+
* required; the rest either select the kind of caller (`tokenStore` versus
|
|
1986
|
+
* `serverKey`) or replace a global the SDK would otherwise reach for.
|
|
1987
|
+
*/
|
|
1896
1988
|
interface FleetlessClientOptions {
|
|
1897
1989
|
/** Base URL of the Fleetless REST API, e.g. `https://api.fleetless.dev`. */
|
|
1898
1990
|
apiUrl: string;
|
|
@@ -1918,27 +2010,50 @@ interface FleetlessClientOptions {
|
|
|
1918
2010
|
/** Defaults to `apiUrl` with http(s) swapped for ws(s) and `/realtime` appended. */
|
|
1919
2011
|
realtimeUrl?: string;
|
|
1920
2012
|
}
|
|
2013
|
+
/**
|
|
2014
|
+
* The settled configuration of a client, reachable as `client.config`. It
|
|
2015
|
+
* is frozen and reflects the defaults `createClient` filled in, which is
|
|
2016
|
+
* what makes it worth reading: `realtimeUrl` is usually derived rather than
|
|
2017
|
+
* passed.
|
|
2018
|
+
*/
|
|
1921
2019
|
interface FleetlessClientConfig {
|
|
2020
|
+
/** The REST base URL this client calls, exactly as passed to `createClient`. */
|
|
1922
2021
|
readonly apiUrl: string;
|
|
2022
|
+
/** The app this client acts as, exactly as passed to `createClient`. */
|
|
1923
2023
|
readonly appIdentifier: string;
|
|
2024
|
+
/** The realtime WebSocket URL in use, derived from `apiUrl` unless one was passed. */
|
|
1924
2025
|
readonly realtimeUrl: string;
|
|
1925
2026
|
}
|
|
2027
|
+
/**
|
|
2028
|
+
* One app's client, returned by `createClient`. Every API the SDK offers is
|
|
2029
|
+
* a property on it, and all of them share this client's identity, its
|
|
2030
|
+
* single realtime channel and its token refresh.
|
|
2031
|
+
*/
|
|
1926
2032
|
interface FleetlessClient {
|
|
2033
|
+
/** The settled configuration, including the defaults `createClient` filled in. */
|
|
1927
2034
|
readonly config: FleetlessClientConfig;
|
|
2035
|
+
/**
|
|
2036
|
+
* The whole client auth API: registration, verification, login, logout,
|
|
2037
|
+
* password reset, invitations, the app's federated sign-in providers, the
|
|
2038
|
+
* MCP consent screen, and the app user's own standing MCP grants.
|
|
2039
|
+
*/
|
|
1928
2040
|
readonly auth: AuthApi;
|
|
1929
|
-
/**
|
|
1930
|
-
readonly grants: GrantsApi;
|
|
2041
|
+
/** A topic's latest value, a live subscription to it, and its recorded history. */
|
|
1931
2042
|
readonly datapoints: DatapointsApi;
|
|
2043
|
+
/** Long-running work on the robot: invoke, cancel, and watch a job as it runs. */
|
|
1932
2044
|
readonly actions: ActionsApi;
|
|
2045
|
+
/** Request/response calls to the robot that answer once and are done. */
|
|
1933
2046
|
readonly services: ServicesApi;
|
|
2047
|
+
/** One-way messages to a robot's publisher, such as a velocity command. */
|
|
1934
2048
|
readonly publishers: PublishersApi;
|
|
2049
|
+
/** Camera snapshots, their age, and live video sessions. */
|
|
1935
2050
|
readonly cameras: CamerasApi;
|
|
1936
2051
|
/**
|
|
1937
2052
|
* Robot-wide job reads that do not fit under `actions`/`services` because
|
|
1938
2053
|
* they are not addressed by slug — see `JobsApi.list`.
|
|
1939
2054
|
*/
|
|
1940
2055
|
readonly jobs: JobsApi;
|
|
1941
|
-
/** URDF
|
|
2056
|
+
/** URDF and mesh reads — list/get/urdf, plus the `urdf-loader` mesh callback. */
|
|
1942
2057
|
readonly assets: AssetsApi;
|
|
1943
2058
|
/**
|
|
1944
2059
|
* Closes the realtime channel and stops it from reconnecting. Safe to
|
|
@@ -1951,6 +2066,16 @@ interface FleetlessClient {
|
|
|
1951
2066
|
*/
|
|
1952
2067
|
close(): void;
|
|
1953
2068
|
}
|
|
2069
|
+
/**
|
|
2070
|
+
* Builds a client for one app. Pass `tokenStore` (or nothing — the default
|
|
2071
|
+
* keeps the session in memory) for an app-user client that signs in with
|
|
2072
|
+
* `auth.login` or a federated provider; pass `serverKey` for a server-side
|
|
2073
|
+
* caller that never holds a user session. Passing both throws, because the
|
|
2074
|
+
* two are different identities and a client acts as exactly one.
|
|
2075
|
+
*
|
|
2076
|
+
* Nothing is fetched here: the realtime channel opens on the first
|
|
2077
|
+
* subscription and closes on `close()` or `auth.logout()`.
|
|
2078
|
+
*/
|
|
1954
2079
|
declare function createClient(options: FleetlessClientOptions): FleetlessClient;
|
|
1955
2080
|
|
|
1956
|
-
export { type ActionsApi, type Asset, type AssetBytes, type AssetListResponse, type AssetsApi, type AuthApi, type
|
|
2081
|
+
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 };
|