@fleetless/sdk 1.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/README.md +1217 -0
- package/dist/index.cjs +3662 -0
- package/dist/index.d.cts +1681 -0
- package/dist/index.d.ts +1681 -0
- package/dist/index.js +3631 -0
- package/package.json +42 -0
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,1681 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Jobs (spec §6.1, §11.3): one running unit of work on a robot — an action
|
|
5
|
+
* goal or a service call — with an id both sides know, so bridge and cloud
|
|
6
|
+
* stay in sync across a disconnect.
|
|
7
|
+
*
|
|
8
|
+
* Two rules shape everything here:
|
|
9
|
+
*
|
|
10
|
+
* 1. **State is observed by slug, not by id.** The id is informative (§11.3);
|
|
11
|
+
* a client watches `robot × slug` and sees whatever job is running there,
|
|
12
|
+
* which is also why every observer of a slug sees the same job.
|
|
13
|
+
* 2. **`lost` is a real outcome and must be said out loud** (§6.1). Job state
|
|
14
|
+
* lives only in the bridge's memory; if it restarts mid-job, the results
|
|
15
|
+
* are gone. The cloud then marks the job `lost` — never leaves it reading
|
|
16
|
+
* "running" because nobody contradicted it. A system that reports a
|
|
17
|
+
* machine is still working when it does not know is worse than one that
|
|
18
|
+
* admits it lost track.
|
|
19
|
+
*/
|
|
20
|
+
declare const jobState: z.ZodEnum<{
|
|
21
|
+
running: "running";
|
|
22
|
+
succeeded: "succeeded";
|
|
23
|
+
failed: "failed";
|
|
24
|
+
cancelled: "cancelled";
|
|
25
|
+
lost: "lost";
|
|
26
|
+
}>;
|
|
27
|
+
type JobState = z.infer<typeof jobState>;
|
|
28
|
+
declare const job: z.ZodObject<{
|
|
29
|
+
id: z.ZodUUID;
|
|
30
|
+
robot_id: z.ZodUUID;
|
|
31
|
+
slug: z.ZodString;
|
|
32
|
+
state: z.ZodEnum<{
|
|
33
|
+
running: "running";
|
|
34
|
+
succeeded: "succeeded";
|
|
35
|
+
failed: "failed";
|
|
36
|
+
cancelled: "cancelled";
|
|
37
|
+
lost: "lost";
|
|
38
|
+
}>;
|
|
39
|
+
started_at: z.ZodISODateTime;
|
|
40
|
+
updated_at: z.ZodISODateTime;
|
|
41
|
+
seq: z.ZodNumber;
|
|
42
|
+
result: z.ZodNullable<z.ZodUnknown>;
|
|
43
|
+
error: z.ZodNullable<z.ZodObject<{
|
|
44
|
+
code: z.ZodString;
|
|
45
|
+
message: z.ZodString;
|
|
46
|
+
details: z.ZodOptional<z.ZodUnknown>;
|
|
47
|
+
}, z.core.$strip>>;
|
|
48
|
+
}, z.core.$strip>;
|
|
49
|
+
type Job = z.infer<typeof job>;
|
|
50
|
+
/**
|
|
51
|
+
* One update about a job, pushed to subscribers of its slug.
|
|
52
|
+
*
|
|
53
|
+
* `timestamp_ms` is the bridge's capture time, exactly as for a datapoint
|
|
54
|
+
* (§6.3 says action feedback carries it too) — so a client computes the age
|
|
55
|
+
* of a progress report the same way it computes the age of a sensor value,
|
|
56
|
+
* and a burst of late-delivered feedback after a reconnect is visibly late
|
|
57
|
+
* rather than looking current.
|
|
58
|
+
*/
|
|
59
|
+
declare const jobEvent: z.ZodObject<{
|
|
60
|
+
type: z.ZodLiteral<"job">;
|
|
61
|
+
robot_id: z.ZodUUID;
|
|
62
|
+
slug: z.ZodString;
|
|
63
|
+
job: z.ZodObject<{
|
|
64
|
+
id: z.ZodUUID;
|
|
65
|
+
robot_id: z.ZodUUID;
|
|
66
|
+
slug: z.ZodString;
|
|
67
|
+
state: z.ZodEnum<{
|
|
68
|
+
running: "running";
|
|
69
|
+
succeeded: "succeeded";
|
|
70
|
+
failed: "failed";
|
|
71
|
+
cancelled: "cancelled";
|
|
72
|
+
lost: "lost";
|
|
73
|
+
}>;
|
|
74
|
+
started_at: z.ZodISODateTime;
|
|
75
|
+
updated_at: z.ZodISODateTime;
|
|
76
|
+
seq: z.ZodNumber;
|
|
77
|
+
result: z.ZodNullable<z.ZodUnknown>;
|
|
78
|
+
error: z.ZodNullable<z.ZodObject<{
|
|
79
|
+
code: z.ZodString;
|
|
80
|
+
message: z.ZodString;
|
|
81
|
+
details: z.ZodOptional<z.ZodUnknown>;
|
|
82
|
+
}, z.core.$strip>>;
|
|
83
|
+
}, z.core.$strip>;
|
|
84
|
+
feedback: z.ZodNullable<z.ZodUnknown>;
|
|
85
|
+
progress: z.ZodNullable<z.ZodNumber>;
|
|
86
|
+
timestamp_ms: z.ZodNumber;
|
|
87
|
+
}, z.core.$strip>;
|
|
88
|
+
type JobEvent = z.infer<typeof jobEvent>;
|
|
89
|
+
/**
|
|
90
|
+
* What a busy refusal tells the caller (spec §11.3: "inkl. Information, was
|
|
91
|
+
* läuft"). A refusal that only says "busy" forces the caller to guess whether
|
|
92
|
+
* to wait or to give up.
|
|
93
|
+
*/
|
|
94
|
+
declare const busyDetails: z.ZodObject<{
|
|
95
|
+
running: z.ZodObject<{
|
|
96
|
+
id: z.ZodUUID;
|
|
97
|
+
robot_id: z.ZodUUID;
|
|
98
|
+
slug: z.ZodString;
|
|
99
|
+
state: z.ZodEnum<{
|
|
100
|
+
running: "running";
|
|
101
|
+
succeeded: "succeeded";
|
|
102
|
+
failed: "failed";
|
|
103
|
+
cancelled: "cancelled";
|
|
104
|
+
lost: "lost";
|
|
105
|
+
}>;
|
|
106
|
+
started_at: z.ZodISODateTime;
|
|
107
|
+
updated_at: z.ZodISODateTime;
|
|
108
|
+
seq: z.ZodNumber;
|
|
109
|
+
result: z.ZodNullable<z.ZodUnknown>;
|
|
110
|
+
error: z.ZodNullable<z.ZodObject<{
|
|
111
|
+
code: z.ZodString;
|
|
112
|
+
message: z.ZodString;
|
|
113
|
+
details: z.ZodOptional<z.ZodUnknown>;
|
|
114
|
+
}, z.core.$strip>>;
|
|
115
|
+
}, z.core.$strip>;
|
|
116
|
+
}, z.core.$strip>;
|
|
117
|
+
type BusyDetails = z.infer<typeof busyDetails>;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The REST read of one datapoint. For bridge-captured data `timestamp_ms`
|
|
121
|
+
* is the capture time at the bridge (spec §6.3); for the cloud-observed
|
|
122
|
+
* built-in `bridge-state` it is the time the cloud observed the state.
|
|
123
|
+
*/
|
|
124
|
+
declare const datapointValue: z.ZodObject<{
|
|
125
|
+
slug: z.ZodString;
|
|
126
|
+
value: z.ZodUnknown;
|
|
127
|
+
timestamp_ms: z.ZodNumber;
|
|
128
|
+
}, z.core.$strip>;
|
|
129
|
+
type DatapointValue = z.infer<typeof datapointValue>;
|
|
130
|
+
/**
|
|
131
|
+
* Every job the platform currently believes this robot has — `GET
|
|
132
|
+
* /api/robots/:id/jobs` (W6b).
|
|
133
|
+
*
|
|
134
|
+
* `jobResponse` answers "what is on this slug", which requires knowing the
|
|
135
|
+
* slug first. That was enough while a job could only exist on a slug the
|
|
136
|
+
* published configuration named. W6b breaks that assumption twice: a
|
|
137
|
+
* reconnecting bridge can name a job the cloud has **no row for** and the
|
|
138
|
+
* cloud adopts it, and a configuration change can leave a job on a slug the
|
|
139
|
+
* document no longer contains. Both are jobs nobody can ask about, because
|
|
140
|
+
* asking requires already knowing what to ask for.
|
|
141
|
+
*
|
|
142
|
+
* So this route exists to answer the question the per-slug route cannot: not
|
|
143
|
+
* "is something running here", but "what is this robot doing". A restarted
|
|
144
|
+
* cloud that has just reconciled a robot's `hello.active_jobs` has exactly
|
|
145
|
+
* this list and, until now, no way to say it out loud.
|
|
146
|
+
*
|
|
147
|
+
* The array is ordered newest first and is **never null**: a robot doing
|
|
148
|
+
* nothing answers `{ jobs: [] }`. "Nothing is running" and "we did not look"
|
|
149
|
+
* are different facts, and a nullable list would merge them — the same
|
|
150
|
+
* distinction `robotDeletionSummary` was made all-required for.
|
|
151
|
+
*
|
|
152
|
+
* **At most one entry per slug: the current job there, exactly what
|
|
153
|
+
* `jobResponse` would answer for that slug.** This is not a history endpoint
|
|
154
|
+
* and must not become one. The first implementation returned every job the
|
|
155
|
+
* registry still held — six rows and four complete Fibonacci results after a
|
|
156
|
+
* few minutes of gate traffic, and unbounded in both count and payload for a
|
|
157
|
+
* robot that has been working all day. The list would have grown until a
|
|
158
|
+
* console page carried a robot's entire past, and the one thing it exists to
|
|
159
|
+
* answer — *what is this robot doing* — would have been the first line of a
|
|
160
|
+
* scroll.
|
|
161
|
+
*
|
|
162
|
+
* A settled job stays visible as its slug's current entry until something
|
|
163
|
+
* else runs there, which is what makes a job that just failed still findable.
|
|
164
|
+
* Read `state` to tell a live one from a finished one, exactly as with
|
|
165
|
+
* `jobResponse`.
|
|
166
|
+
*/
|
|
167
|
+
/**
|
|
168
|
+
* What a `rate_limited` refusal tells the caller (W6c).
|
|
169
|
+
*
|
|
170
|
+
* One number, and it is the only one that matters: **when to come back.** A
|
|
171
|
+
* limit that says "too many" without saying "in 800 ms" produces a client that
|
|
172
|
+
* retries immediately, which is the behaviour the limit exists to stop — so
|
|
173
|
+
* omitting it would make the refusal part of the attack.
|
|
174
|
+
*
|
|
175
|
+
* Deliberately **not** carrying the limit, the window, or how many attempts
|
|
176
|
+
* remain: those describe the defence to whoever is probing it, and none of
|
|
177
|
+
* them changes what an honest caller does.
|
|
178
|
+
*/
|
|
179
|
+
declare const rateLimitDetails: z.ZodObject<{
|
|
180
|
+
retry_after_ms: z.ZodNumber;
|
|
181
|
+
}, z.core.$strip>;
|
|
182
|
+
type RateLimitDetails = z.infer<typeof rateLimitDetails>;
|
|
183
|
+
declare const cameraDescriptor: z.ZodObject<{
|
|
184
|
+
slug: z.ZodString;
|
|
185
|
+
width: z.ZodNumber;
|
|
186
|
+
height: z.ZodNumber;
|
|
187
|
+
fps: z.ZodNumber;
|
|
188
|
+
snapshot_interval_ms: z.ZodNumber;
|
|
189
|
+
}, z.core.$strip>;
|
|
190
|
+
type CameraDescriptor = z.infer<typeof cameraDescriptor>;
|
|
191
|
+
/**
|
|
192
|
+
* Raw samples. `timestamp_ms` is the **bridge's capture time** (§6.3) — the
|
|
193
|
+
* same instant the live value carried, so a recorded point and a live one can
|
|
194
|
+
* be placed on one axis without apology.
|
|
195
|
+
*
|
|
196
|
+
* `truncated` says the response was cut short. A short array that does not
|
|
197
|
+
* admit it is indistinguishable from a quiet period, and the two lead a
|
|
198
|
+
* developer to opposite conclusions.
|
|
199
|
+
*/
|
|
200
|
+
declare const historySamplesResponse: z.ZodObject<{
|
|
201
|
+
slug: z.ZodString;
|
|
202
|
+
kind: z.ZodLiteral<"samples">;
|
|
203
|
+
samples: z.ZodArray<z.ZodObject<{
|
|
204
|
+
timestamp_ms: z.ZodNumber;
|
|
205
|
+
value: z.ZodUnknown;
|
|
206
|
+
}, z.core.$strip>>;
|
|
207
|
+
truncated: z.ZodBoolean;
|
|
208
|
+
truncated_by: z.ZodNullable<z.ZodEnum<{
|
|
209
|
+
limit: "limit";
|
|
210
|
+
bytes: "bytes";
|
|
211
|
+
}>>;
|
|
212
|
+
}, z.core.$strip>;
|
|
213
|
+
type HistorySamplesResponse = z.infer<typeof historySamplesResponse>;
|
|
214
|
+
/**
|
|
215
|
+
* Aggregated buckets — a **separate shape**, not the samples shape with nulls
|
|
216
|
+
* in it, so a client knows by type what it received rather than by
|
|
217
|
+
* inspection.
|
|
218
|
+
*
|
|
219
|
+
* `sample_count` exists because an empty bucket and a bucket whose average is
|
|
220
|
+
* zero are different facts. W5 established at some cost what happens when two
|
|
221
|
+
* facts share one representation, and a chart is the easiest place in this
|
|
222
|
+
* product to draw a gap as a line.
|
|
223
|
+
*/
|
|
224
|
+
declare const historyBucketsResponse: z.ZodObject<{
|
|
225
|
+
slug: z.ZodString;
|
|
226
|
+
kind: z.ZodLiteral<"buckets">;
|
|
227
|
+
window_ms: z.ZodNumber;
|
|
228
|
+
agg: z.ZodEnum<{
|
|
229
|
+
min: "min";
|
|
230
|
+
max: "max";
|
|
231
|
+
avg: "avg";
|
|
232
|
+
}>;
|
|
233
|
+
buckets: z.ZodArray<z.ZodObject<{
|
|
234
|
+
bucket_start_ms: z.ZodNumber;
|
|
235
|
+
value: z.ZodNullable<z.ZodNumber>;
|
|
236
|
+
sample_count: z.ZodNumber;
|
|
237
|
+
}, z.core.$strip>>;
|
|
238
|
+
}, z.core.$strip>;
|
|
239
|
+
type HistoryBucketsResponse = z.infer<typeof historyBucketsResponse>;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* One datapoint sample pushed to a subscriber. The current value arrives
|
|
243
|
+
* immediately on subscribe, then every change. `timestamp_ms` semantics as
|
|
244
|
+
* in `datapointValue` (capture time; cloud-observed for `bridge-state`).
|
|
245
|
+
*/
|
|
246
|
+
declare const datapointEvent: z.ZodObject<{
|
|
247
|
+
type: z.ZodLiteral<"datapoint">;
|
|
248
|
+
robot_id: z.ZodUUID;
|
|
249
|
+
slug: z.ZodString;
|
|
250
|
+
value: z.ZodUnknown;
|
|
251
|
+
timestamp_ms: z.ZodNumber;
|
|
252
|
+
}, z.core.$strip>;
|
|
253
|
+
type DatapointEvent = z.infer<typeof datapointEvent>;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Access plus refresh (spec §3.4). The access token is short-lived; the
|
|
257
|
+
* refresh token rotates on every use, so a stolen one is detectable when the
|
|
258
|
+
* original is presented again.
|
|
259
|
+
*/
|
|
260
|
+
declare const sessionTokens: z.ZodObject<{
|
|
261
|
+
access_token: z.ZodString;
|
|
262
|
+
refresh_token: z.ZodString;
|
|
263
|
+
expires_in: z.ZodNumber;
|
|
264
|
+
}, z.core.$strip>;
|
|
265
|
+
type SessionTokens = z.infer<typeof sessionTokens>;
|
|
266
|
+
/**
|
|
267
|
+
* An invitation carries its own accept URL: the link is the primary path
|
|
268
|
+
* (the developer shares it), mail is the second. A cloud with no SMTP
|
|
269
|
+
* configured still issues invitations — it just cannot send them, and says so
|
|
270
|
+
* via `mail_sent`.
|
|
271
|
+
*/
|
|
272
|
+
/**
|
|
273
|
+
* What happened to the mail, in three words instead of one (W6c).
|
|
274
|
+
*
|
|
275
|
+
* `mail_sent: boolean` could not tell **"we have no SMTP configured"** from
|
|
276
|
+
* **"we tried and the server refused"**, so the console had to pick a sentence
|
|
277
|
+
* for a cause it could not know — and picked the reassuring one, because a
|
|
278
|
+
* link-only invitation is a normal outcome and a bounced one is not. The two
|
|
279
|
+
* need opposite actions from whoever reads them: configure a mail server, or
|
|
280
|
+
* go and look at why the existing one rejected the message.
|
|
281
|
+
*
|
|
282
|
+
* - `sent` — the SMTP server accepted the message. Not "delivered":
|
|
283
|
+
* no sender can promise that, and this value must never
|
|
284
|
+
* be rendered as if it could.
|
|
285
|
+
* - `not_configured` — no SMTP is set up. **An expected state, not a failure**
|
|
286
|
+
* (spec §3.2: invitations work without mail; the link is
|
|
287
|
+
* the primary path). The console must not show it as an
|
|
288
|
+
* error.
|
|
289
|
+
* - `failed` — SMTP was configured, was tried, and refused or was
|
|
290
|
+
* unreachable. This one is worth someone's attention.
|
|
291
|
+
*/
|
|
292
|
+
declare const mailStatus: z.ZodEnum<{
|
|
293
|
+
failed: "failed";
|
|
294
|
+
sent: "sent";
|
|
295
|
+
not_configured: "not_configured";
|
|
296
|
+
}>;
|
|
297
|
+
type MailStatus = z.infer<typeof mailStatus>;
|
|
298
|
+
/**
|
|
299
|
+
* What registering answers — deliberately **the same for an address that is
|
|
300
|
+
* new and one that already has an account**.
|
|
301
|
+
*
|
|
302
|
+
* Anything else is an account-enumeration oracle on an unauthenticated route,
|
|
303
|
+
* the same reasoning `passwordResetRequest` carries. An address that already
|
|
304
|
+
* exists still gets a mail, saying so; the caller cannot tell which mail was
|
|
305
|
+
* sent, and there is nothing in this response to tell them.
|
|
306
|
+
*
|
|
307
|
+
* `mail` is safe to return because it describes **the server's configuration**,
|
|
308
|
+
* not the address: `not_configured` means this deployment has no SMTP, which
|
|
309
|
+
* is true regardless of who registered. Note that a deployment with no mail
|
|
310
|
+
* server cannot complete a self-registration at all — the link is the only way
|
|
311
|
+
* through, unlike an invitation, where a developer can hand it over directly.
|
|
312
|
+
*/
|
|
313
|
+
declare const clientRegisterResponse: z.ZodObject<{
|
|
314
|
+
mail: z.ZodEnum<{
|
|
315
|
+
failed: "failed";
|
|
316
|
+
sent: "sent";
|
|
317
|
+
not_configured: "not_configured";
|
|
318
|
+
}>;
|
|
319
|
+
}, z.core.$strip>;
|
|
320
|
+
type ClientRegisterResponse = z.infer<typeof clientRegisterResponse>;
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Who the caller turned out to be. Returned by the "who am I" endpoint and by
|
|
324
|
+
* the realtime `auth_ok` frame, so a client can render a session without
|
|
325
|
+
* decoding a token itself — decoding a JWT in the client is how apps end up
|
|
326
|
+
* trusting claims nobody verified.
|
|
327
|
+
*
|
|
328
|
+
* **Three kinds of caller reach the client API, not two.** Besides end users
|
|
329
|
+
* and server keys, a **developer** does: spec §15.2 says the console's
|
|
330
|
+
* playground runs over the real client API and appears in the audit as the
|
|
331
|
+
* developer, and the console's own live views (robot list badges, the Live
|
|
332
|
+
* tab) subscribe on `/realtime` as one. A developer is **org-scoped, not
|
|
333
|
+
* app-scoped** — they own the configuration of every robot in their org — so
|
|
334
|
+
* `app_id` and `role_id` are null for them, and roles do not filter what they
|
|
335
|
+
* see. `kind` states this explicitly rather than leaving it to be inferred
|
|
336
|
+
* from which id happens to be set.
|
|
337
|
+
*/
|
|
338
|
+
declare const clientIdentity: z.ZodObject<{
|
|
339
|
+
kind: z.ZodEnum<{
|
|
340
|
+
server_key: "server_key";
|
|
341
|
+
developer: "developer";
|
|
342
|
+
end_user: "end_user";
|
|
343
|
+
}>;
|
|
344
|
+
developer_id: z.ZodNullable<z.ZodUUID>;
|
|
345
|
+
end_user_id: z.ZodNullable<z.ZodUUID>;
|
|
346
|
+
server_key_id: z.ZodNullable<z.ZodUUID>;
|
|
347
|
+
app_id: z.ZodNullable<z.ZodUUID>;
|
|
348
|
+
role_id: z.ZodNullable<z.ZodUUID>;
|
|
349
|
+
email: z.ZodNullable<z.ZodEmail>;
|
|
350
|
+
}, z.core.$strip>;
|
|
351
|
+
type ClientIdentity = z.infer<typeof clientIdentity>;
|
|
352
|
+
|
|
353
|
+
declare const asset: z.ZodObject<{
|
|
354
|
+
id: z.ZodUUID;
|
|
355
|
+
robot_id: z.ZodUUID;
|
|
356
|
+
kind: z.ZodEnum<{
|
|
357
|
+
urdf: "urdf";
|
|
358
|
+
mesh: "mesh";
|
|
359
|
+
texture: "texture";
|
|
360
|
+
other: "other";
|
|
361
|
+
}>;
|
|
362
|
+
name: z.ZodString;
|
|
363
|
+
media_type: z.ZodString;
|
|
364
|
+
size_bytes: z.ZodNumber;
|
|
365
|
+
sha256: z.ZodString;
|
|
366
|
+
created_at: z.ZodISODateTime;
|
|
367
|
+
}, z.core.$strip>;
|
|
368
|
+
type Asset = z.infer<typeof asset>;
|
|
369
|
+
/**
|
|
370
|
+
* Whether a URDF can actually be rendered, which is not the same as whether it
|
|
371
|
+
* was uploaded.
|
|
372
|
+
*
|
|
373
|
+
* `missing` carries **the reference, verbatim, that no asset answers** — for
|
|
374
|
+
* a `package://` mesh the URI the bridge could not resolve in the workspace,
|
|
375
|
+
* and since W7's security fix also the absolute paths and bare relative paths
|
|
376
|
+
* a URDF may carry, which the extractor sees and the sync deliberately never
|
|
377
|
+
* offers. The sentence used to say "the `package://` URIs" and the field
|
|
378
|
+
* carried three kinds of string (Momus-W7); it is widened here rather than
|
|
379
|
+
* narrowed, because a developer whose URDF names `/opt/meshes/arm.stl` is
|
|
380
|
+
* entitled to be told that nothing will ever fetch it.
|
|
381
|
+
*
|
|
382
|
+
* The spec's example is "2 Meshes fehlen" and that number alone is a dead
|
|
383
|
+
* end: it tells a developer to go looking through a workspace by hand. The
|
|
384
|
+
* references are what they can act on, so the references travel.
|
|
385
|
+
*
|
|
386
|
+
* **Every entry must be actionable, and that is a constraint on the
|
|
387
|
+
* producers, not on this field (W7a).** An entry a developer cannot make
|
|
388
|
+
* disappear by fixing what it names is a defect in whoever put it there: for
|
|
389
|
+
* a whole wave `<texture>` references were listed here and no sync would ever
|
|
390
|
+
* offer them, so the honest instruction behind the list was "fix this, it
|
|
391
|
+
* will not help".
|
|
392
|
+
*/
|
|
393
|
+
declare const urdfCompleteness: z.ZodObject<{
|
|
394
|
+
present: z.ZodBoolean;
|
|
395
|
+
mesh_count: z.ZodNumber;
|
|
396
|
+
missing: z.ZodArray<z.ZodString>;
|
|
397
|
+
}, z.core.$strip>;
|
|
398
|
+
type UrdfCompleteness = z.infer<typeof urdfCompleteness>;
|
|
399
|
+
declare const assetListResponse: z.ZodObject<{
|
|
400
|
+
assets: z.ZodArray<z.ZodObject<{
|
|
401
|
+
id: z.ZodUUID;
|
|
402
|
+
robot_id: z.ZodUUID;
|
|
403
|
+
kind: z.ZodEnum<{
|
|
404
|
+
urdf: "urdf";
|
|
405
|
+
mesh: "mesh";
|
|
406
|
+
texture: "texture";
|
|
407
|
+
other: "other";
|
|
408
|
+
}>;
|
|
409
|
+
name: z.ZodString;
|
|
410
|
+
media_type: z.ZodString;
|
|
411
|
+
size_bytes: z.ZodNumber;
|
|
412
|
+
sha256: z.ZodString;
|
|
413
|
+
created_at: z.ZodISODateTime;
|
|
414
|
+
}, z.core.$strip>>;
|
|
415
|
+
urdf: z.ZodObject<{
|
|
416
|
+
present: z.ZodBoolean;
|
|
417
|
+
mesh_count: z.ZodNumber;
|
|
418
|
+
missing: z.ZodArray<z.ZodString>;
|
|
419
|
+
}, z.core.$strip>;
|
|
420
|
+
urdf_available: z.ZodNullable<z.ZodBoolean>;
|
|
421
|
+
}, z.core.$strip>;
|
|
422
|
+
type AssetListResponse = z.infer<typeof assetListResponse>;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* One violated §4.4 rule. `details` on the envelope stays `unknown` — codes
|
|
426
|
+
* are an open set, so their payloads cannot all be enumerated — but the
|
|
427
|
+
* payload of `parameter_invalid` **is** pinned here, because otherwise every
|
|
428
|
+
* consumer guesses: the cloud emits one shape, the SDK sniffs for two, the
|
|
429
|
+
* console renders a third, and each is right in its own tests.
|
|
430
|
+
*
|
|
431
|
+
* `field` is the **flat key exactly as the caller sent it** — the same string
|
|
432
|
+
* as the `parameterSpec.name` it violated. That is the entire justification
|
|
433
|
+
* for the flat parameter form: a refusal has to name something the caller can
|
|
434
|
+
* find in what they typed, and a console can attach the error to that one
|
|
435
|
+
* input rather than to the form.
|
|
436
|
+
*/
|
|
437
|
+
declare const parameterViolation: z.ZodObject<{
|
|
438
|
+
field: z.ZodString;
|
|
439
|
+
rule: z.ZodString;
|
|
440
|
+
message: z.ZodString;
|
|
441
|
+
}, z.core.$strip>;
|
|
442
|
+
type ParameterViolation = z.infer<typeof parameterViolation>;
|
|
443
|
+
/**
|
|
444
|
+
* The `details` of a `parameter_invalid` refusal. Always at least one
|
|
445
|
+
* violation: a refusal that names none would leave the caller with nothing to
|
|
446
|
+
* fix. All violations are reported at once, not just the first — a caller
|
|
447
|
+
* fixing parameters one round-trip at a time is a caller who gives up.
|
|
448
|
+
*/
|
|
449
|
+
declare const parameterInvalidDetails: z.ZodObject<{
|
|
450
|
+
violations: z.ZodArray<z.ZodObject<{
|
|
451
|
+
field: z.ZodString;
|
|
452
|
+
rule: z.ZodString;
|
|
453
|
+
message: z.ZodString;
|
|
454
|
+
}, z.core.$strip>>;
|
|
455
|
+
}, z.core.$strip>;
|
|
456
|
+
type ParameterInvalidDetails = z.infer<typeof parameterInvalidDetails>;
|
|
457
|
+
/**
|
|
458
|
+
* The codes in use as of W2. The wire deliberately allows any string — this
|
|
459
|
+
* list is the shared vocabulary, not a closed set, so a new refusal never
|
|
460
|
+
* needs a contracts release before it can be reported honestly.
|
|
461
|
+
*/
|
|
462
|
+
declare const ERROR_CODES: readonly ["not_found", "validation_error", "bad_request", "unknown_datapoint", "invalid_token", "protocol_mismatch", "invalid_frame", "duplicate_slug", "reserved_slug", "unknown_field_path", "unknown_type", "unknown_topic", "invalid_rate", "invalid_range", "config_conflict", "no_data", "robot_offline", "bridge_timeout", "unauthorized", "forbidden", "invalid_credentials", "token_expired", "token_revoked", "invite_expired", "invite_used", "email_taken", "identifier_taken", "weak_password", "not_a_member", "account_blocked", "busy", "parameter_invalid", "job_lost", "publisher_busy", "unknown_command", "not_subscribable", "camera_offline", "no_snapshot_yet", "live_unavailable", "wrong_kind", "not_recorded", "not_aggregatable", "quota_exceeded", "credential_in_use", "goal_timeout", "robot_in_use", "robot_deletion_partial", "job_queue_full", "invalid_uuid", "rate_limited", "tier_required", "token_spent", "service_timeout", "asset_missing", "asset_too_large", "dynamic_registration_disabled", "client_limit_reached", "identity_conflict", "identity_not_provisioned", "idp_unavailable", "mcp_disabled", "tool_not_available"];
|
|
463
|
+
type ErrorCode = (typeof ERROR_CODES)[number];
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Codes the SDK produces itself rather than relaying from the server. Kept
|
|
467
|
+
* out of `@fleetless/contracts`' `ERROR_CODES` deliberately — that list is
|
|
468
|
+
* the *wire* vocabulary, every entry something a server may actually send,
|
|
469
|
+
* and none of these are. Two kinds:
|
|
470
|
+
*
|
|
471
|
+
* - `no_session` / `no_websocket`: a client-side refusal *before* a request
|
|
472
|
+
* ever reaches the network (not logged in; no WebSocket implementation
|
|
473
|
+
* available). Named apart from the server's own `unauthorized` so a
|
|
474
|
+
* caller can tell "the server refused me" from "the SDK refused before
|
|
475
|
+
* asking" by the code alone.
|
|
476
|
+
* - `unparseable_error`: the opposite direction — a real response *did*
|
|
477
|
+
* arrive, its body just wasn't shaped like §11.5's error format. Not a
|
|
478
|
+
* refusal at all, just "we don't know what the server said."
|
|
479
|
+
* - `command_timeout` (W4): a realtime command (`invoke`/`cancel`/`publish`)
|
|
480
|
+
* got no `command_result` within its timeout. The server may still answer
|
|
481
|
+
* later on the same socket — nobody knows — but the caller cannot be made
|
|
482
|
+
* to wait forever for that.
|
|
483
|
+
* - `command_outcome_unknown` (W4): worse than a timeout, and told apart
|
|
484
|
+
* from it on purpose — the realtime connection that carried the command
|
|
485
|
+
* was replaced by a new one (a reconnect) before any reply arrived. A
|
|
486
|
+
* reply can now never come: the server, if it answered at all, answered a
|
|
487
|
+
* socket that no longer exists. The command may or may not have run.
|
|
488
|
+
* Never retried automatically — that could run an action twice — the
|
|
489
|
+
* caller recovers by reading the job (e.g. `actions.subscribe`), since
|
|
490
|
+
* state is observed by slug regardless of which connection asked for it.
|
|
491
|
+
* - `unexpected_response` (W4): the server answered `ok:true` but left out
|
|
492
|
+
* something the command is defined to always return (e.g. no `job` on a
|
|
493
|
+
* successful `invoke`) — a contract violation the SDK noticed, not a
|
|
494
|
+
* refusal.
|
|
495
|
+
* - `invalid_option` (W6b): the caller passed an *SDK-level* argument or
|
|
496
|
+
* option that cannot mean what it looks like it means. Two cases so far:
|
|
497
|
+
* `timeoutMs < patienceMs` on `invoke`/`call` (see `resolveLocalWaitMs` in
|
|
498
|
+
* `commands.ts`) — the SDK would give up locally before the platform's own
|
|
499
|
+
* patience runs out, and report `command_timeout` for a call the platform
|
|
500
|
+
* never actually refused; and a non-string, non-null, non-omitted `jobId`
|
|
501
|
+
* on `cancel` (D6, see `assertValidJobId`) — almost always a caller who
|
|
502
|
+
* upgraded past the pre-W6b `cancel(robotId, slug, options?)` signature
|
|
503
|
+
* and is still passing an options object third. Both are thrown
|
|
504
|
+
* synchronously, before any request is sent — a client-side mistake to
|
|
505
|
+
* fix, not something a server response could ever produce, which is why
|
|
506
|
+
* this code belongs here and not in `@fleetless/contracts`' `ERROR_CODES`.
|
|
507
|
+
* - `untrusted_absolute_url` (W7): `HttpClient` refused to fetch an absolute
|
|
508
|
+
* URL whose origin does not match this client's own configured `baseUrl`
|
|
509
|
+
* — thrown before the request is ever sent, so no `Authorization` header
|
|
510
|
+
* is ever built for it, let alone attached. The one caller that hands
|
|
511
|
+
* `HttpClient` an absolute URL at all is `assets.createMeshLoader`
|
|
512
|
+
* (§4.6), fetching a URDF's rewritten mesh URIs — and a URDF is ROS graph
|
|
513
|
+
* input, not first-party data, so an app rendering one must not silently
|
|
514
|
+
* trust wherever it points. `assets.createMeshLoader`'s `onComplete`
|
|
515
|
+
* surfaces this the same way it surfaces a network failure: `(null, err)`.
|
|
516
|
+
* - `no_urdf_synced` (W7a): `assets.prepareUrdfScene` looked for a
|
|
517
|
+
* `kind: 'urdf'` row in `assets.list()` and found none. Thrown before any
|
|
518
|
+
* asset fetch, rather than left to surface as a confusing downstream
|
|
519
|
+
* failure from `URDFLoader.parse(undefined)` or similar — the caller's
|
|
520
|
+
* fix is "sync a URDF first" (console, Owner-tier, §4.6), which this
|
|
521
|
+
* error can say directly.
|
|
522
|
+
* - `no_hosted_login_attempt` (W7b, Momus's review): `auth.completeHostedLogin()`
|
|
523
|
+
* was called with an empty `expectedState` — nothing was persisted for
|
|
524
|
+
* this attempt. A callback landing in a different tab or window than the
|
|
525
|
+
* one that called `beginHostedLogin`, a restored session, or storage
|
|
526
|
+
* cleared in between all produce exactly this, and none of them is an
|
|
527
|
+
* attack. Told apart from `state_mismatch` on purpose: the two diagnoses
|
|
528
|
+
* have different remedies ("check how you persisted the value" versus
|
|
529
|
+
* "this response belongs to a login you did not start"), the same
|
|
530
|
+
* reasoning this wave already applied once to `identity_conflict` versus
|
|
531
|
+
* `identity_not_provisioned`. Also closes a real gap — comparing two
|
|
532
|
+
* *empty* strings with `!==` is `false`, so without this check first, a
|
|
533
|
+
* caller with nothing persisted at all could reach `state_mismatch`'s
|
|
534
|
+
* comparison having contributed no defence whatsoever.
|
|
535
|
+
* - `aborted` (register row 244, W7c): `assets.prepareUrdfScene()` was given
|
|
536
|
+
* an `AbortSignal` and it fired — either already-aborted before the call
|
|
537
|
+
* started, or mid-flight while a fetch was in progress. Normalized to this
|
|
538
|
+
* one code regardless of which stage the abort landed in, rather than
|
|
539
|
+
* surfacing whatever shape the underlying `fetch()` rejects an aborted
|
|
540
|
+
* request with (a `DOMException` named `AbortError` in a browser, an
|
|
541
|
+
* `Error` named `AbortError` under Node's `fetch` — two different shapes a
|
|
542
|
+
* caller would otherwise have to detect themselves to tell "I cancelled
|
|
543
|
+
* this" from "the network actually failed"). Every partial resource this
|
|
544
|
+
* call had already created (`blob:` URLs) is revoked before this throws —
|
|
545
|
+
* an aborted load must not leak what it fetched before the signal fired,
|
|
546
|
+
* the same guarantee a failed load already had (D6).
|
|
547
|
+
* - `state_mismatch` (W7b): `auth.completeHostedLogin()` was called with a
|
|
548
|
+
* `state` that does not match the `expectedState` its own `beginHostedLogin()`
|
|
549
|
+
* returned for this attempt (or with no `state` at all — `beginHostedLogin`
|
|
550
|
+
* always sets one, so a callback carrying none does not look like a reply
|
|
551
|
+
* to a flow this client started). Thrown before `/oauth/token` is ever
|
|
552
|
+
* called — RFC 6749 §10.12's whole point is that a caller must not
|
|
553
|
+
* complete an authorization response it did not itself request, so this
|
|
554
|
+
* check happens client-side, first, rather than being left to the server
|
|
555
|
+
* to catch (by which point a code exchange would already have been
|
|
556
|
+
* attempted for a flow this client never started).
|
|
557
|
+
*/
|
|
558
|
+
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"];
|
|
559
|
+
type SdkErrorCode = (typeof SDK_ERROR_CODES)[number];
|
|
560
|
+
/**
|
|
561
|
+
* A stable code a caller can branch on: a server-defined code (open-ended —
|
|
562
|
+
* see `ErrorCode`'s own doc comment), one of the SDK's own client-side
|
|
563
|
+
* codes above, or, since neither list is exhaustive, any other string.
|
|
564
|
+
* `(string & {})` is the standard trick to keep autocomplete on the known
|
|
565
|
+
* values while still accepting an arbitrary one.
|
|
566
|
+
*/
|
|
567
|
+
type FleetlessErrorCode = ErrorCode | SdkErrorCode | (string & {});
|
|
568
|
+
/**
|
|
569
|
+
* The one error type the SDK throws for a refused API call (spec §11.5): a
|
|
570
|
+
* stable machine-readable `code` a caller can branch on (`forbidden` vs
|
|
571
|
+
* `token_expired`) plus a human `message` for logs and debugging. Never
|
|
572
|
+
* parse `message` — it is not part of the contract, only `code` is.
|
|
573
|
+
*/
|
|
574
|
+
interface FleetlessErrorOptions {
|
|
575
|
+
/** Field-level detail for validation errors, passed through verbatim. */
|
|
576
|
+
details?: unknown;
|
|
577
|
+
/** The HTTP status of the response that produced this error, if any. */
|
|
578
|
+
status?: number;
|
|
579
|
+
}
|
|
580
|
+
declare class FleetlessError extends Error {
|
|
581
|
+
readonly code: FleetlessErrorCode;
|
|
582
|
+
readonly details?: unknown;
|
|
583
|
+
readonly status?: number;
|
|
584
|
+
constructor(code: FleetlessErrorCode, message: string, options?: FleetlessErrorOptions);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
interface SendCommandOptions {
|
|
588
|
+
/** How long to wait for a `command_result` before rejecting `command_timeout`. Default 10s. */
|
|
589
|
+
timeoutMs?: number;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* `invoke`-only: how long **the platform itself** should wait for this one
|
|
593
|
+
* call before giving up on the robot (W6b) — the whole wait for a service
|
|
594
|
+
* call, goal *acceptance* only for an action (once accepted, a job runs as
|
|
595
|
+
* long as it runs and is observed, not awaited).
|
|
596
|
+
*
|
|
597
|
+
* Optional; absent means the platform's own default (`DEFAULT_PATIENCE_MS`,
|
|
598
|
+
* 15s) — exactly today's behaviour for a caller who names no preference.
|
|
599
|
+
* Outside `[MIN_PATIENCE_MS, MAX_PATIENCE_MS]` the platform refuses with
|
|
600
|
+
* `validation_error` rather than clamping, and this SDK does not clamp
|
|
601
|
+
* locally or retry — surfacing the refusal is the whole of what it does
|
|
602
|
+
* with this field. The floor exists because impatience reaches the robot,
|
|
603
|
+
* not just the platform: a patience too short to survive a goal-acceptance
|
|
604
|
+
* round trip made the bridge report `goal_timeout` and then issue a
|
|
605
|
+
* *corrective cancel* against a goal an action server accepted a moment
|
|
606
|
+
* later — a caller who names an unreachable deadline was causing a real
|
|
607
|
+
* cancellation on the machine, repeatably, not just receiving an error.
|
|
608
|
+
*
|
|
609
|
+
* `timeoutMs` bounds how long *this SDK* waits locally for a reply on the
|
|
610
|
+
* wire it already sent on; `patienceMs` travels to the platform and bounds
|
|
611
|
+
* what *it* is willing to wait for from the robot. **They are not set
|
|
612
|
+
* independently of each other, and D3a exists because the first version of
|
|
613
|
+
* this doc comment said they were.** `timeoutMs` left unset is *derived*
|
|
614
|
+
* from `patienceMs` (see `resolveLocalWaitMs`), not defaulted to a fixed
|
|
615
|
+
* number that might be shorter — the SDK giving up locally before the
|
|
616
|
+
* platform's own deadline would report `command_timeout` for a call the
|
|
617
|
+
* platform never actually refused, which is exactly the two-clocks defect
|
|
618
|
+
* this wave exists to remove, just relocated into this SDK instead of
|
|
619
|
+
* between the cloud and the bridge. Setting both explicitly with
|
|
620
|
+
* `timeoutMs < patienceMs` is refused with `invalid_option` before any
|
|
621
|
+
* request is sent, for the same reason.
|
|
622
|
+
*/
|
|
623
|
+
interface InvokeOptions extends SendCommandOptions {
|
|
624
|
+
patienceMs?: number;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
interface JobSubscriptionHandlers {
|
|
628
|
+
/** Called on every update pushed for the slug's current job — state, feedback, progress and result. */
|
|
629
|
+
onJob(event: JobEvent): void;
|
|
630
|
+
/** Called once if the subscription is refused (§11.5: e.g. `forbidden`, an unknown slug). */
|
|
631
|
+
onError?(error: FleetlessError): void;
|
|
632
|
+
}
|
|
633
|
+
interface JobSubscription {
|
|
634
|
+
/** Stops the subscription and, if the channel is currently connected, tells the server. */
|
|
635
|
+
unsubscribe(): void;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
interface ActionsApi {
|
|
639
|
+
/**
|
|
640
|
+
* Invokes an action (spec §11.3). Resolves as soon as the job is created
|
|
641
|
+
* — the job id is informative, not the result. Feedback, progress and the
|
|
642
|
+
* eventual result arrive separately over `subscribe`. A second invoke of
|
|
643
|
+
* the same slug while one is already running is refused `busy`, with
|
|
644
|
+
* `error.details.running` naming the job that is running.
|
|
645
|
+
*
|
|
646
|
+
* `options.patienceMs` bounds goal *acceptance* only (W6b) — once a goal
|
|
647
|
+
* is accepted this call has already resolved; the job then runs as long
|
|
648
|
+
* as it runs, observed via `subscribe`, never awaited. `options.timeoutMs`
|
|
649
|
+
* (this SDK's own local wait for the acceptance reply) is derived from
|
|
650
|
+
* `patienceMs` when left unset, and the combination `timeoutMs <
|
|
651
|
+
* patienceMs` is refused with `invalid_option` rather than raced — see
|
|
652
|
+
* `resolveLocalWaitMs` in `commands.ts`, D3a.
|
|
653
|
+
*/
|
|
654
|
+
invoke(robotId: string, slug: string, params: Record<string, unknown>, options?: InvokeOptions): Promise<Job>;
|
|
655
|
+
/**
|
|
656
|
+
* Cancels a job — a real ROS goal cancel on the robot, not a local forget.
|
|
657
|
+
* Resolves with the `Job` the cancel was actually sent to, or `null` if
|
|
658
|
+
* nothing matched.
|
|
659
|
+
*
|
|
660
|
+
* **Two different requests, both legitimate (W6b):**
|
|
661
|
+
* - `cancel(robotId, slug)` — no `jobId` — is the operator's stop button:
|
|
662
|
+
* whatever is running on this slug, stop it. This is unchanged from
|
|
663
|
+
* before W6b.
|
|
664
|
+
* - `cancel(robotId, slug, jobId)` cancels **that** job specifically. If
|
|
665
|
+
* it is not the one running, the platform answers `not_found` — this
|
|
666
|
+
* never silently falls back to stopping whatever *is* running, because
|
|
667
|
+
* a caller who named an id has already ruled that out. The failure this
|
|
668
|
+
* closes: a cancel arriving just after its own job ended used to stop
|
|
669
|
+
* the *next* caller's job on the same slug.
|
|
670
|
+
*
|
|
671
|
+
* Read the returned job either way: "I stopped the one I meant", "there
|
|
672
|
+
* was nothing there", and "I stopped a job that started after I last
|
|
673
|
+
* looked" are three different outcomes a discarded result cannot tell
|
|
674
|
+
* apart.
|
|
675
|
+
*/
|
|
676
|
+
cancel(robotId: string, slug: string, jobId?: string | null, options?: SendCommandOptions): Promise<Job | null>;
|
|
677
|
+
/**
|
|
678
|
+
* Subscribes to the slug's job: state, feedback, progress and result, as
|
|
679
|
+
* they happen. State is observed **by slug**, not by job id (§11.3) — this
|
|
680
|
+
* is what makes late delivery after a reconnect and a second observer
|
|
681
|
+
* watching the same job both work without special-casing either. Naming a
|
|
682
|
+
* job to `cancel` does not change this: a slug is still a *place a job may
|
|
683
|
+
* be running*, not the job itself, and it is still what `subscribe` watches.
|
|
684
|
+
*/
|
|
685
|
+
subscribe(robotId: string, slug: string, handlers: JobSubscriptionHandlers): JobSubscription;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** An asset's bytes plus its declared media type — the shape `assets.get` answers with. */
|
|
689
|
+
interface AssetBytes {
|
|
690
|
+
body: Uint8Array;
|
|
691
|
+
mime: string | null;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* The signature `urdf-loader`'s own `loadMeshCb` uses (verified against that
|
|
695
|
+
* library's docs, not guessed): `manager`/`material` pass straight through
|
|
696
|
+
* from whatever called this, and `onComplete` is how a mesh loader reports
|
|
697
|
+
* success or failure — it never throws.
|
|
698
|
+
*
|
|
699
|
+
* This SDK does not depend on `urdf-loader` or three.js — `manager`,
|
|
700
|
+
* `material` and the resolved object are all opaque here, exactly what makes
|
|
701
|
+
* `createMeshLoader` usable from any renderer that speaks this same shape,
|
|
702
|
+
* not only that one library.
|
|
703
|
+
*/
|
|
704
|
+
type MeshLoaderDelegate = (path: string, manager: unknown, material: unknown, onComplete: (obj: unknown | null, err?: Error) => void) => void;
|
|
705
|
+
interface CreateMeshLoaderOptions {
|
|
706
|
+
/**
|
|
707
|
+
* How long to wait for `delegate`'s `onComplete` before giving up.
|
|
708
|
+
* Defaults to 30s. A URDF pulls in thirty-odd meshes and a dashboard
|
|
709
|
+
* using this callback can run for days — a delegate that never calls back
|
|
710
|
+
* (an exception three.js swallowed internally, a parser stuck on a
|
|
711
|
+
* malformed mesh) must not leak the object URL or hang the load forever.
|
|
712
|
+
*/
|
|
713
|
+
timeoutMs?: number;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* three.js's own `LoadingManager.setURLModifier(callback)` shape (W7a, D2).
|
|
717
|
+
* Every load the manager oversees is routed through `callback` first — not
|
|
718
|
+
* only the loader you handed the manager to, but every loader it constructs
|
|
719
|
+
* internally on the same manager (`ColladaLoader`'s own `TextureLoader` for
|
|
720
|
+
* a `.dae`'s `<init_from>` images, in particular). That is the one hook
|
|
721
|
+
* that exists "one level up, for everything" (D2) where `createMeshLoader`'s
|
|
722
|
+
* per-loader `loadMeshCb` override does not reach: `TextureLoader` has no
|
|
723
|
+
* override hook of its own.
|
|
724
|
+
*
|
|
725
|
+
* Structural, not `import('three')` — this SDK does not depend on three.js
|
|
726
|
+
* or `urdf-loader`, same discipline as `MeshLoaderDelegate` above.
|
|
727
|
+
*/
|
|
728
|
+
interface UrdfSceneManager {
|
|
729
|
+
setURLModifier(callback: (url: string) => string): unknown;
|
|
730
|
+
}
|
|
731
|
+
interface PrepareUrdfSceneOptions {
|
|
732
|
+
/**
|
|
733
|
+
* How many assets to fetch in parallel. Default 6 — a default that keeps
|
|
734
|
+
* memory and connection counts sane for the common case (dozens of
|
|
735
|
+
* meshes), not a number with a sweep behind it. Pass your own if you have
|
|
736
|
+
* a reason to.
|
|
737
|
+
*
|
|
738
|
+
* Must be a positive integer — `0` or negative throws `invalid_option`
|
|
739
|
+
* rather than silently fetching nothing and returning a scene that
|
|
740
|
+
* renders completely blank with no error to explain why.
|
|
741
|
+
*/
|
|
742
|
+
concurrency?: number;
|
|
743
|
+
/**
|
|
744
|
+
* Cancels this call (register row 244, W7c) — a caller who navigates away
|
|
745
|
+
* or switches to a different robot mid-load can abort every in-flight
|
|
746
|
+
* fetch this method has started, not merely stop it from starting new
|
|
747
|
+
* ones. Checked before the first request; if it fires while a request is
|
|
748
|
+
* already in progress, `HttpClient` forwards it straight to `fetch()`
|
|
749
|
+
* (`http.ts`'s own `RequestOptions.signal`), so the connection itself is
|
|
750
|
+
* torn down, not just abandoned by this SDK while it keeps running in the
|
|
751
|
+
* background.
|
|
752
|
+
*
|
|
753
|
+
* Every `blob:` URL already created before the abort is revoked before
|
|
754
|
+
* this call rejects with `FleetlessError('aborted', ...)` — the same
|
|
755
|
+
* guarantee a load that fails outright already had (D6): an aborted load
|
|
756
|
+
* must not leak what it had already fetched.
|
|
757
|
+
*
|
|
758
|
+
* `manager`'s URL modifier is only ever installed once every asset has
|
|
759
|
+
* resolved (success or the throw below) — an aborted call never installs
|
|
760
|
+
* a partial one, so `manager` is left exactly as it was if this rejects
|
|
761
|
+
* before that point.
|
|
762
|
+
*/
|
|
763
|
+
signal?: AbortSignal;
|
|
764
|
+
}
|
|
765
|
+
interface UrdfSceneResources {
|
|
766
|
+
/**
|
|
767
|
+
* The robot's URDF as raw text — `package://` URIs intact, not rewritten
|
|
768
|
+
* to asset URLs. Hand it straight to `URDFLoader.parse(urdfText)` once
|
|
769
|
+
* `setURLModifier` is installed (this method already installed it on
|
|
770
|
+
* `manager` before returning).
|
|
771
|
+
*/
|
|
772
|
+
urdfText: string;
|
|
773
|
+
/**
|
|
774
|
+
* The same reference strings `assets.list()`'s `urdf.missing` reports.
|
|
775
|
+
*
|
|
776
|
+
* **Top-level only — not a `.dae`'s internal references (Kassandra-W7a
|
|
777
|
+
* review correction).** The cloud builds this list from the URDF text
|
|
778
|
+
* alone (`<mesh>`/`<texture>` `filename` attributes), which is the only
|
|
779
|
+
* place it can see without parsing every `.dae` a sync touches; it never
|
|
780
|
+
* has and never can include an internal `<init_from>` reference. An
|
|
781
|
+
* earlier version of this comment claimed both — wrong, and worth naming
|
|
782
|
+
* as a correction rather than silently widening the sentence, since a
|
|
783
|
+
* caller who trusted "both" to mean both would build a completeness check
|
|
784
|
+
* against a list that structurally cannot report the second half. A
|
|
785
|
+
* `.dae`-internal reference the sync could not resolve surfaces through
|
|
786
|
+
* the sync's own failure reporting instead, not here.
|
|
787
|
+
*
|
|
788
|
+
* Not thrown either way: an incomplete URDF still renders what it has
|
|
789
|
+
* (`urdfCompleteness`'s own contract), so the caller decides whether to
|
|
790
|
+
* warn, block, or ignore before calling `URDFLoader.parse(urdfText)`. A
|
|
791
|
+
* reference NOT in this list that still fails to load at render time is a
|
|
792
|
+
* different failure — the store answered but the fetch itself did not.
|
|
793
|
+
*/
|
|
794
|
+
missing: string[];
|
|
795
|
+
/**
|
|
796
|
+
* Revokes every `blob:` URL this call created. Call once the scene has
|
|
797
|
+
* finished loading (success or failure) or on unmount — safe to call more
|
|
798
|
+
* than once.
|
|
799
|
+
*
|
|
800
|
+
* **Does not touch `manager`'s URL modifier (D7, Kassandra-W7a review).**
|
|
801
|
+
* An earlier version reset it to the identity function here, which
|
|
802
|
+
* silently reopened D4 the moment the same manager was used again — for a
|
|
803
|
+
* second robot, or for anything else — before a later `prepareUrdfScene`
|
|
804
|
+
* call happened to overwrite it. The installed modifier is left running,
|
|
805
|
+
* and with this call's map now empty it already refuses anything it would
|
|
806
|
+
* have owned and passes through anything it would not have, correctly,
|
|
807
|
+
* on its own.
|
|
808
|
+
*/
|
|
809
|
+
dispose(): void;
|
|
810
|
+
}
|
|
811
|
+
interface AssetsApi {
|
|
812
|
+
/**
|
|
813
|
+
* Every asset a robot has, plus whether its URDF is complete (spec §4.6).
|
|
814
|
+
* `urdf.missing` names the `package://` URIs the sync could not resolve —
|
|
815
|
+
* the number alone ("2 Meshes fehlen") sends a developer looking through a
|
|
816
|
+
* workspace by hand, the URIs are what they can act on.
|
|
817
|
+
*/
|
|
818
|
+
list(robotId: string): Promise<AssetListResponse>;
|
|
819
|
+
/** One asset's bytes by id — a mesh, or any asset directly, addressed the same way `createMeshLoader` reaches one internally. */
|
|
820
|
+
get(robotId: string, assetId: string): Promise<AssetBytes>;
|
|
821
|
+
/**
|
|
822
|
+
* The robot's URDF, with every `package://` mesh URI already rewritten to
|
|
823
|
+
* an absolute Fleetless asset URL — ready to hand straight to
|
|
824
|
+
* `URDFLoader.parse(xml)`. Decoded as UTF-8 text rather than left as
|
|
825
|
+
* bytes because every consumer needs it as a string for exactly that call.
|
|
826
|
+
*/
|
|
827
|
+
urdf(robotId: string): Promise<string>;
|
|
828
|
+
/**
|
|
829
|
+
* The mesh callback for `urdf-loader` (spec §4.6): an `<img>` tag and the
|
|
830
|
+
* default three.js loaders cannot set an `Authorization` header, and the
|
|
831
|
+
* platform deliberately has no signed URLs and no token in the query
|
|
832
|
+
* string (see `@fleetless/contracts` `assets.ts`), so every app would
|
|
833
|
+
* otherwise write this glue itself, and each one differently.
|
|
834
|
+
*
|
|
835
|
+
* Returns a function with `loadMeshCb`'s own signature — assign it
|
|
836
|
+
* directly:
|
|
837
|
+
*
|
|
838
|
+
* ```ts
|
|
839
|
+
* loader.loadMeshCb = client.assets.createMeshLoader(robotId, loader.defaultMeshLoader.bind(loader))
|
|
840
|
+
* ```
|
|
841
|
+
*
|
|
842
|
+
* `delegate` does the actual format-specific parsing (STL/OBJ/DAE/GLTF —
|
|
843
|
+
* `loader.defaultMeshLoader` already knows how); this method's own job is
|
|
844
|
+
* only what a plain loader cannot do: fetch `path` with the
|
|
845
|
+
* `Authorization` header, and hand the delegate something it can load
|
|
846
|
+
* without one. It does that by fetching the bytes itself, wrapping them
|
|
847
|
+
* in a `Blob`, and calling `delegate` with an object URL substituted for
|
|
848
|
+
* `path` — so the delegate never touches the network, and the object URL
|
|
849
|
+
* is revoked the moment `delegate` reports success or failure (or the
|
|
850
|
+
* timeout elapses), never left for the caller to remember.
|
|
851
|
+
*
|
|
852
|
+
* **Refuses, via `onComplete(null, err)`, if the `manager` it is handed
|
|
853
|
+
* already has `prepareUrdfScene`'s URL modifier installed** — the two
|
|
854
|
+
* read different URDF sources and combining them on one manager breaks
|
|
855
|
+
* one half or the other, never obviously (see `prepareUrdfScene`'s own
|
|
856
|
+
* doc comment). Checked at the point the mistake would actually manifest
|
|
857
|
+
* rather than left to a paragraph a developer might not read.
|
|
858
|
+
*/
|
|
859
|
+
createMeshLoader(robotId: string, delegate: MeshLoaderDelegate, options?: CreateMeshLoaderOptions): MeshLoaderDelegate;
|
|
860
|
+
/**
|
|
861
|
+
* Authenticated loading for everything three.js fetches to render a
|
|
862
|
+
* textured robot — not only meshes (spec §4.6, W7a, D2). Installs
|
|
863
|
+
* `manager.setURLModifier` so **every** load `manager` oversees resolves
|
|
864
|
+
* to a pre-fetched `blob:` URL: a top-level `<mesh>`, a `<material>`'s
|
|
865
|
+
* `<texture>`, and an image a `.dae` references internally via
|
|
866
|
+
* `<init_from>` — three cases, one mechanism, because the browser never
|
|
867
|
+
* fetches an asset directly. Every network read goes through this SDK
|
|
868
|
+
* with the bearer token first; a `blob:` URL is document-local and dies
|
|
869
|
+
* with the page, so nothing about who may read what changes.
|
|
870
|
+
*
|
|
871
|
+
* ```ts
|
|
872
|
+
* const manager = new THREE.LoadingManager()
|
|
873
|
+
* const { urdfText, missing, dispose } = await client.assets.prepareUrdfScene(robotId, manager)
|
|
874
|
+
* const loader = new URDFLoader(manager)
|
|
875
|
+
* const robot = loader.parse(urdfText)
|
|
876
|
+
* scene.add(robot)
|
|
877
|
+
* // later, once the scene has finished loading (or on unmount):
|
|
878
|
+
* dispose()
|
|
879
|
+
* ```
|
|
880
|
+
*
|
|
881
|
+
* **Do not also install `createMeshLoader` on the same manager.** The two
|
|
882
|
+
* consume different URDF sources — this method fetches the URDF's *raw*
|
|
883
|
+
* bytes, `createMeshLoader` is meant to pair with `urdf()`'s
|
|
884
|
+
* cloud-rewritten text. **Not "double-fetches every mesh" — a first
|
|
885
|
+
* version of this comment said that, and Momus-W7a's review traced it
|
|
886
|
+
* and found it wrong.** What actually happens is asymmetric breakage,
|
|
887
|
+
* whichever URDF text the combination ends up parsing: paired with
|
|
888
|
+
* *this* method's raw text, `createMeshLoader` receives urdf-loader's
|
|
889
|
+
* `resolvePath()` output (`/pkg/rel`) rather than an absolute Fleetless
|
|
890
|
+
* URL and 404s every mesh, while textures still resolve; paired with
|
|
891
|
+
* `urdf()`'s rewritten text instead, meshes load and every texture 401s
|
|
892
|
+
* — `createMeshLoader` bypasses this method's URL modifier entirely for
|
|
893
|
+
* meshes (that's what `loadMeshCb` means), so there is no hook left for
|
|
894
|
+
* a texture. Either way a developer who combined them by accident would
|
|
895
|
+
* debug the wrong symptom, which is worse than the original (already
|
|
896
|
+
* wrong) warning being merely unhelpful.
|
|
897
|
+
*
|
|
898
|
+
* **Enforced, not only documented (Momus-W7a review, via the team
|
|
899
|
+
* lead).** `createMeshLoader`'s returned callback checks whether the
|
|
900
|
+
* `manager` it is handed already has this method's URL modifier
|
|
901
|
+
* installed and fails loudly via `onComplete(null, err)` before ever
|
|
902
|
+
* touching the network, rather than relying on a developer having read
|
|
903
|
+
* this paragraph. See `managersWithPreparedUrdfScene`.
|
|
904
|
+
*
|
|
905
|
+
* **Why raw bytes, not `urdf()`'s rewritten text.** Both `urdf-loader`'s
|
|
906
|
+
* default mesh loading and `ColladaLoader` compute the base path they use
|
|
907
|
+
* to resolve a `.dae`'s internal references (`LoaderUtils.extractUrlBase`)
|
|
908
|
+
* from the URL they were originally asked to load — *before*
|
|
909
|
+
* `manager.resolveURL()`/the URL modifier ever runs; the modifier only
|
|
910
|
+
* changes what bytes get fetched, never what further relative references
|
|
911
|
+
* resolve against. Feeding it `urdf()`'s already-rewritten
|
|
912
|
+
* `.../assets/<uuid>` text would compute a base of `.../assets/`, and
|
|
913
|
+
* `textures/skin.png` joined onto that would never match anything this
|
|
914
|
+
* method's map knows about. Raw `package://` text is what keeps the two
|
|
915
|
+
* in sync.
|
|
916
|
+
*
|
|
917
|
+
* **`urdf-loader` resolves `package://` itself, before any of this runs —
|
|
918
|
+
* a second resolution stage this method has to account for, found by
|
|
919
|
+
* Threepio-W7a running the recipe demo in a real browser rather than
|
|
920
|
+
* reading the source.** `URDFLoader.parse()`'s own `resolvePath()`
|
|
921
|
+
* rewrites `package://pkg/rel` using `this.packages` (default `''`) to
|
|
922
|
+
* `/pkg/rel` — a root-relative URL — and *that* is what reaches
|
|
923
|
+
* `loadMeshCb`/`ColladaLoader`/`manager.resolveURL()`, not the original
|
|
924
|
+
* string. So the modifier is registered under **two** keys per asset: the
|
|
925
|
+
* literal `package://` name (`asset.name`, for a caller who sets
|
|
926
|
+
* `loader.packages = (pkg) => \`package://\${pkg}\`` to reconstruct it,
|
|
927
|
+
* or a renderer that never went through `resolvePath()` at all) and the
|
|
928
|
+
* root-relative form `urdf-loader`'s own *default* `packages: ''`
|
|
929
|
+
* produces (`/pkg/rel`) — covering the common case with zero required
|
|
930
|
+
* caller configuration. A `.dae`'s internal `<init_from>` ref resolves the
|
|
931
|
+
* same way one level deeper: `ColladaLoader` computes its own working
|
|
932
|
+
* path from *its* `url` argument (already `/pkg/rel` by the time it gets
|
|
933
|
+
* there under the default), so the internal reference lands on
|
|
934
|
+
* `/pkg/textures/skin.png` — exactly the second key, derived the same
|
|
935
|
+
* way. A caller whose `loader.packages` does something else entirely
|
|
936
|
+
* (a custom map, not the default and not the reconstruction above) is
|
|
937
|
+
* outside what this method can predict — see the ownership rule below
|
|
938
|
+
* for what happens to that reference.
|
|
939
|
+
*
|
|
940
|
+
* **This method only claims what it owns (D4, revised after Argus-W7a's
|
|
941
|
+
* review) — not every unmapped reference.** `manager` is frequently the
|
|
942
|
+
* caller's own scene-wide `LoadingManager`, shared for an HDRI, an
|
|
943
|
+
* environment map, a font atlas, a ground texture — none of which have
|
|
944
|
+
* anything to do with this robot. A first version refused everything
|
|
945
|
+
* unmapped, which silently emptied every one of those the moment a
|
|
946
|
+
* caller shared their manager. So the rule is narrower: a `package://`
|
|
947
|
+
* reference, or a root-relative path whose leading segment names a ROS
|
|
948
|
+
* package this robot's assets (or `missing`) actually mention, is this
|
|
949
|
+
* method's to resolve or refuse; an unmapped one falls back to the
|
|
950
|
+
* normalized form (D5, below) and then to a shared, inert, page-local
|
|
951
|
+
* `blob:` URL — never the original string, so a hostile URDF naming an
|
|
952
|
+
* unsynced or off-namespace reference still cannot make three.js touch
|
|
953
|
+
* the network for it. Anything else — not in that namespace — is left
|
|
954
|
+
* completely alone, **except** an absolute `http(s)` URL, which is
|
|
955
|
+
* refused regardless of namespace: the one case this method cannot leave
|
|
956
|
+
* ambiguous, because a hostile URDF naming an attacker's host directly
|
|
957
|
+
* (bypassing `package://` entirely) is exactly what D4 exists to close,
|
|
958
|
+
* and three.js would otherwise fetch it for real, off-origin, the moment
|
|
959
|
+
* the direct and namespace checks both miss.
|
|
960
|
+
*
|
|
961
|
+
* **A `.dae`'s own internal reference gets a second-chance, normalized
|
|
962
|
+
* lookup (D5, Momus-W7a review, reproduced in a real browser).** three.js
|
|
963
|
+
* builds the request for one by plain string concatenation — no `..`/`.`
|
|
964
|
+
* collapsing — while `asset.name` carries the *normalized* tail
|
|
965
|
+
* (`@fleetless/contracts`' naming rule). So `../textures/skin.png` or
|
|
966
|
+
* `./textures/skin.png`, both ordinary exporter output, would otherwise
|
|
967
|
+
* miss the direct key even though the reference is entirely resolvable.
|
|
968
|
+
* Verified independently that the top-level `<mesh>`/`<texture>` case
|
|
969
|
+
* never needs this: `asset.name` there is the URDF's own `package://` URI
|
|
970
|
+
* verbatim and `resolvePath()` rewrites it by the same unnormalized
|
|
971
|
+
* concatenation on both sides, so the direct key already matches.
|
|
972
|
+
*
|
|
973
|
+
* **`dispose()` does not undo any of this (D7, Kassandra-W7a review).**
|
|
974
|
+
* See its own doc comment on `UrdfSceneResources`.
|
|
975
|
+
*
|
|
976
|
+
* **Pre-fetch is unavoidable**, not merely a choice: a URL modifier
|
|
977
|
+
* cannot be asynchronous, so every asset it might be asked for has to
|
|
978
|
+
* already be a `blob:` URL before `URDFLoader.parse` runs. Bounded by
|
|
979
|
+
* `options.concurrency` (default 6) and scoped to only `kind: 'mesh'` and
|
|
980
|
+
* `kind: 'texture'` assets — which is already "what the URDF references"
|
|
981
|
+
* (`@fleetless/contracts`' `rest.ts`: a re-sync reconciles, so assets the
|
|
982
|
+
* current URDF no longer references stop belonging to the robot), not an
|
|
983
|
+
* unbounded fetch of everything the robot has ever had.
|
|
984
|
+
*/
|
|
985
|
+
prepareUrdfScene(robotId: string, manager: UrdfSceneManager, options?: PrepareUrdfSceneOptions): Promise<UrdfSceneResources>;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* What is kept between calls to stay logged in: exactly the wire shape
|
|
990
|
+
* `sessionTokens` returns, no derived fields. Refresh is reactive (a call
|
|
991
|
+
* that meets an expired access token refreshes and retries) rather than
|
|
992
|
+
* proactive, so there is no `expires_at` to compute or drift out of sync.
|
|
993
|
+
*/
|
|
994
|
+
type StoredSession = SessionTokens;
|
|
995
|
+
/**
|
|
996
|
+
* Where the SDK keeps a session. A developer implements this to persist a
|
|
997
|
+
* login (localStorage, a cookie, a native keystore) — the SDK itself never
|
|
998
|
+
* assumes a browser, or any storage, exists.
|
|
999
|
+
*/
|
|
1000
|
+
interface TokenStore {
|
|
1001
|
+
load(): StoredSession | null | Promise<StoredSession | null>;
|
|
1002
|
+
save(session: StoredSession | null): void | Promise<void>;
|
|
1003
|
+
}
|
|
1004
|
+
/** The default store: works out of the box, forgets the session on reload. */
|
|
1005
|
+
declare class InMemoryTokenStore implements TokenStore {
|
|
1006
|
+
#private;
|
|
1007
|
+
load(): StoredSession | null;
|
|
1008
|
+
save(session: StoredSession | null): void;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/** `beginHostedLogin()`'s input (spec §3.4, §17, W7b). */
|
|
1012
|
+
interface BeginHostedLoginOptions {
|
|
1013
|
+
/**
|
|
1014
|
+
* The opaque `client_id` issued when the developer registered this app's
|
|
1015
|
+
* OAuth client (console, App Settings). **Never `appIdentifier`** — they
|
|
1016
|
+
* are deliberately different identifiers (`contracts/src/oauth.ts`,
|
|
1017
|
+
* `oauthClient`'s doc comment: "the `client_id` on the wire — opaque, and
|
|
1018
|
+
* not the app identifier").
|
|
1019
|
+
*/
|
|
1020
|
+
clientId: string;
|
|
1021
|
+
/**
|
|
1022
|
+
* Must be registered, byte-for-byte, as one of that client's
|
|
1023
|
+
* `redirect_uris` — matching at the server is exact-string, never a
|
|
1024
|
+
* prefix (`contracts/src/oauth.ts`, `redirectUri`'s doc comment).
|
|
1025
|
+
*/
|
|
1026
|
+
redirectUri: string;
|
|
1027
|
+
scope?: string;
|
|
1028
|
+
/**
|
|
1029
|
+
* RFC 8707 audience binding (register row, W7b→W7c): the resource this
|
|
1030
|
+
* session's token should be usable against — an MCP endpoint
|
|
1031
|
+
* (`/mcp/<app>`, W7c) or another audience-checking resource this
|
|
1032
|
+
* deployment validates. **Omit it for an ordinary app login.** A token
|
|
1033
|
+
* with no `resource` carries no `aud` and works unrestricted against this
|
|
1034
|
+
* app's own REST surface exactly as it always has; that path is
|
|
1035
|
+
* unaffected by this field's existence. Only a caller that is itself
|
|
1036
|
+
* going to present the token to an audience-checking resource needs to
|
|
1037
|
+
* ask for one — and must ask for the *right* one, because `/mcp/<app>`
|
|
1038
|
+
* refuses a token whose `aud` names a different app exactly as hard as it
|
|
1039
|
+
* refuses a token with none at all.
|
|
1040
|
+
*/
|
|
1041
|
+
resource?: string;
|
|
1042
|
+
}
|
|
1043
|
+
/** What `beginHostedLogin()` returns — nothing here has touched the network yet. */
|
|
1044
|
+
interface HostedLoginRequest {
|
|
1045
|
+
/** Send the end user's browser here to start the hosted login page. */
|
|
1046
|
+
url: string;
|
|
1047
|
+
/**
|
|
1048
|
+
* Persist this alongside `codeVerifier` before navigating away, and pass
|
|
1049
|
+
* both back into `completeHostedLogin`. **This SDK does not persist them
|
|
1050
|
+
* for you.** The redirect back to `redirectUri` is a fresh page load for a
|
|
1051
|
+
* browser app — nothing kept in this SDK's own memory survives it (the
|
|
1052
|
+
* same reasoning `client.ts` states for `TokenStore`: "the SDK itself
|
|
1053
|
+
* never assumes a browser, or any storage, exists"). An in-memory default
|
|
1054
|
+
* here would not be merely suboptimal, it would be broken for the primary
|
|
1055
|
+
* use case while looking like it worked for anything that never actually
|
|
1056
|
+
* navigates away. `sessionStorage`, a signed cookie, or a plain variable
|
|
1057
|
+
* (a popup flow that never truly navigates) are all valid — that choice is
|
|
1058
|
+
* the caller's.
|
|
1059
|
+
*/
|
|
1060
|
+
state: string;
|
|
1061
|
+
codeVerifier: string;
|
|
1062
|
+
}
|
|
1063
|
+
/** `completeHostedLogin()`'s input — the redirect back, plus what `beginHostedLogin` returned for this same attempt. */
|
|
1064
|
+
interface CompleteHostedLoginOptions {
|
|
1065
|
+
/** The `code` query parameter from the redirect back to `redirectUri`. */
|
|
1066
|
+
code: string;
|
|
1067
|
+
/** The `state` query parameter from that same redirect. */
|
|
1068
|
+
state: string;
|
|
1069
|
+
/**
|
|
1070
|
+
* The `state` this attempt's `beginHostedLogin` returned. Checked against
|
|
1071
|
+
* `state` above **before any network call** — RFC 6749 §10.12's whole
|
|
1072
|
+
* point is that a caller must not complete an authorization response it
|
|
1073
|
+
* did not itself request.
|
|
1074
|
+
*/
|
|
1075
|
+
expectedState: string;
|
|
1076
|
+
/** The `codeVerifier` this attempt's `beginHostedLogin` returned. */
|
|
1077
|
+
codeVerifier: string;
|
|
1078
|
+
clientId: string;
|
|
1079
|
+
/** Must be the exact same string passed to `beginHostedLogin`. */
|
|
1080
|
+
redirectUri: string;
|
|
1081
|
+
/**
|
|
1082
|
+
* Must be the exact same string passed to `beginHostedLogin`, if any.
|
|
1083
|
+
* Resending it here is not what binds the audience — the server already
|
|
1084
|
+
* bound `resource` to the authorization code at `/oauth/authorize` and
|
|
1085
|
+
* mints `aud` from that stored value regardless of what this call sends —
|
|
1086
|
+
* but RFC 8707 §2 expects a client to name the resource at both steps,
|
|
1087
|
+
* and the cloud rejects a *mismatched* resend outright (`invalid_target`).
|
|
1088
|
+
* Omit it here exactly when it was omitted at `beginHostedLogin`.
|
|
1089
|
+
*/
|
|
1090
|
+
resource?: string;
|
|
1091
|
+
}
|
|
1092
|
+
interface AuthApi {
|
|
1093
|
+
/** Exchanges email + password, and the client's configured app identifier, for a session. */
|
|
1094
|
+
login(email: string, password: string): Promise<void>;
|
|
1095
|
+
/**
|
|
1096
|
+
* Starts the hosted login flow (spec §3.4, §17, W7b): a Fleetless-served
|
|
1097
|
+
* login page an app's end user is redirected to, with optional
|
|
1098
|
+
* per-app IdP federation. Builds the `/oauth/authorize` URL (Authorization
|
|
1099
|
+
* Code + PKCE, S256 only — OAuth 2.1 removes `plain`) and generates the
|
|
1100
|
+
* `state`/`codeVerifier` PKCE and CSRF protection need. **Makes no network
|
|
1101
|
+
* call** — everything here is local, so nothing about the app, the
|
|
1102
|
+
* client, or the redirect URI is validated until the browser actually
|
|
1103
|
+
* reaches `/oauth/authorize`.
|
|
1104
|
+
*
|
|
1105
|
+
* Async only because computing the S256 `code_challenge` needs
|
|
1106
|
+
* `crypto.subtle.digest`, which the Web Crypto API only ever offers as a
|
|
1107
|
+
* promise — there is no synchronous digest to call instead.
|
|
1108
|
+
*/
|
|
1109
|
+
beginHostedLogin(options: BeginHostedLoginOptions): Promise<HostedLoginRequest>;
|
|
1110
|
+
/**
|
|
1111
|
+
* Completes the hosted login flow: checks `state` against `expectedState`
|
|
1112
|
+
* (before any network call — see `CompleteHostedLoginOptions.expectedState`),
|
|
1113
|
+
* exchanges `code` for tokens at `/oauth/token`, and stores them via the
|
|
1114
|
+
* same `tokenStore` `login()` uses.
|
|
1115
|
+
*
|
|
1116
|
+
* **Two distinct refusals before that check, not one.** An empty
|
|
1117
|
+
* `expectedState` throws `no_hosted_login_attempt` — nothing was
|
|
1118
|
+
* persisted for this attempt (a different tab, a restored session,
|
|
1119
|
+
* cleared storage), not necessarily an attack. Only once `expectedState`
|
|
1120
|
+
* is actually present does a mismatch (or a missing `state` on the
|
|
1121
|
+
* callback itself) throw `state_mismatch`. The two are told apart on
|
|
1122
|
+
* purpose: they call for different remedies, and collapsing them would
|
|
1123
|
+
* tell a developer debugging an ordinary storage gap that their app is
|
|
1124
|
+
* under attack.
|
|
1125
|
+
*
|
|
1126
|
+
* Once past that check and the exchange completes, `me()`, `logout()`,
|
|
1127
|
+
* `changePassword()` and silent refresh all behave identically afterwards,
|
|
1128
|
+
* regardless of which flow the session started from. That is the actual
|
|
1129
|
+
* content of §3.4's *"Beide Wege enden im selben Fleetless-Token"*: not
|
|
1130
|
+
* merely that the bytes match, but that every existing code path treats
|
|
1131
|
+
* the result the same way.
|
|
1132
|
+
*
|
|
1133
|
+
* The wire response is RFC 6749 §5.1's envelope (`token_type`, optional
|
|
1134
|
+
* `scope`), not `sessionTokens` — this method normalizes one into the
|
|
1135
|
+
* other before storing. **Refresh needs no separate handling**: the cloud
|
|
1136
|
+
* mints these tokens through the same session mechanism `/api/client/login`
|
|
1137
|
+
* uses (same `refresh_tokens` row, same rotation), so the existing silent
|
|
1138
|
+
* refresh (`/api/client/refresh`) already works for a hosted-login
|
|
1139
|
+
* session — nothing about the origin of a session is tracked or needs to
|
|
1140
|
+
* be.
|
|
1141
|
+
*
|
|
1142
|
+
* Throws with the OAuth error code as `.code` (e.g. `invalid_grant` for an
|
|
1143
|
+
* expired or already-used `code`) if the exchange itself fails — a
|
|
1144
|
+
* different vocabulary from every other method on this interface, because
|
|
1145
|
+
* `/oauth/token` answers in RFC 6749 §5.2's shape, not `apiError`.
|
|
1146
|
+
*
|
|
1147
|
+
* **Makes exactly one request to `/oauth/token` — never retried, no
|
|
1148
|
+
* timeout-and-resend, no internal concurrency of its own.** Stated
|
|
1149
|
+
* because the constraint that matters here is not this method's, it is
|
|
1150
|
+
* the caller's: **never call this a second time for the same `code`
|
|
1151
|
+
* while a first call is still in flight** (a plain "the first attempt
|
|
1152
|
+
* looked like it timed out, so retry" is exactly the shape this warns
|
|
1153
|
+
* against — it is not a defect in this method, since this method itself
|
|
1154
|
+
* has nothing that could ever cause that). André's decision, 2026-08-18:
|
|
1155
|
+
* the platform treats a second presentation of an authorization code as
|
|
1156
|
+
* theft and revokes the whole token family it belongs to, deliberately,
|
|
1157
|
+
* even though a plain double-submission looks identical on the wire —
|
|
1158
|
+
* because the blast radius is bounded (only a caller already holding the
|
|
1159
|
+
* correct `code_verifier` and `client_id` can trigger it, so a merely
|
|
1160
|
+
* *sniffed* code cannot lock anyone out) and the alternative is a
|
|
1161
|
+
* narrower defence against a real theft.
|
|
1162
|
+
*
|
|
1163
|
+
* **What actually happens if two requests race (measured, Argus-W7c):**
|
|
1164
|
+
* one of the two receives `200` with a refresh token that the server has
|
|
1165
|
+
* already revoked. The access token in that same response keeps working
|
|
1166
|
+
* normally for the rest of its short TTL — nothing about the race is
|
|
1167
|
+
* visible yet. The failure surfaces at this session's **first silent
|
|
1168
|
+
* refresh**, as `token_revoked`, potentially many minutes after the race
|
|
1169
|
+
* that actually caused it and with nothing in that later error pointing
|
|
1170
|
+
* back to a retry that "worked". If your own framework, an HTTP client
|
|
1171
|
+
* wrapper, or a user's impatient double-click can cause this method to
|
|
1172
|
+
* be invoked twice concurrently for the same redirect, guard against
|
|
1173
|
+
* that at the call site — a simple in-flight flag or disabling the
|
|
1174
|
+
* triggering control is enough, since there is only ever one legitimate
|
|
1175
|
+
* exchange per authorization code.
|
|
1176
|
+
*/
|
|
1177
|
+
completeHostedLogin(options: CompleteHostedLoginOptions): Promise<void>;
|
|
1178
|
+
/**
|
|
1179
|
+
* Ends the session: revokes the whole refresh-token family server-side
|
|
1180
|
+
* (a stolen refresh token stops working immediately) and closes this
|
|
1181
|
+
* client's live realtime connection, if it has one. Then clears the
|
|
1182
|
+
* local store. Never rejects and always clears the store, even if the
|
|
1183
|
+
* server call fails: a user who presses "log out" must end up logged out
|
|
1184
|
+
* locally regardless of the network. `revoked` reports whether the
|
|
1185
|
+
* server-side revoke actually happened — `false` means the refresh
|
|
1186
|
+
* family may still be alive server-side even though this client has
|
|
1187
|
+
* forgotten it; an app that cares (a kiosk, a shared workstation) can
|
|
1188
|
+
* warn the user or retry, one that doesn't can ignore it.
|
|
1189
|
+
*
|
|
1190
|
+
* **What this does not do:** invalidate the access token already issued.
|
|
1191
|
+
* Access-token checks are stateless (a signed JWT, verified without a
|
|
1192
|
+
* server-side lookup) — logout has nothing to flip on that token, only
|
|
1193
|
+
* on the refresh family behind it. A token stolen before logout keeps
|
|
1194
|
+
* working on REST, and can still open a *new* realtime connection, until
|
|
1195
|
+
* it expires on its own — at most 15 minutes. This is a deliberate
|
|
1196
|
+
* boundary of the stateless-JWT design (the same one that lets a role
|
|
1197
|
+
* change, a block, or a membership removal take effect on the very next
|
|
1198
|
+
* request without a fresh token), not a bug — but a kiosk or shared
|
|
1199
|
+
* workstation needs to know that number.
|
|
1200
|
+
*/
|
|
1201
|
+
logout(): Promise<{
|
|
1202
|
+
revoked: boolean;
|
|
1203
|
+
}>;
|
|
1204
|
+
/** Who the caller turned out to be, without decoding a token client-side. */
|
|
1205
|
+
me(): Promise<ClientIdentity>;
|
|
1206
|
+
/**
|
|
1207
|
+
* Starts self-registration into this app's pool (spec §3.2, W6c) — the
|
|
1208
|
+
* second of the pool's two entry paths, alongside a console invitation.
|
|
1209
|
+
* Sets the password here; `confirmRegistration` below only ever sees a
|
|
1210
|
+
* token.
|
|
1211
|
+
*
|
|
1212
|
+
* **This does not log the caller in — there is no session yet.** A domain
|
|
1213
|
+
* filter says *which* domains may register, never *whether the caller
|
|
1214
|
+
* owns the address*, so minting a session here would let anybody who
|
|
1215
|
+
* knows an allowed domain register as somebody else at it and receive
|
|
1216
|
+
* whatever role the app assigns — which on this platform can mean
|
|
1217
|
+
* permission to move a robot. So this mails a confirmation link and waits;
|
|
1218
|
+
* `confirmRegistration(token)` is what proves the address and returns a
|
|
1219
|
+
* session, the same two-step shape as `requestPasswordReset` /
|
|
1220
|
+
* `confirmPasswordReset` below.
|
|
1221
|
+
*
|
|
1222
|
+
* **Resolves identically for an address that is new and one that already
|
|
1223
|
+
* has an account** — same account-enumeration reasoning as
|
|
1224
|
+
* `requestPasswordReset`. `mail` in the resolved value describes this
|
|
1225
|
+
* deployment's mail configuration, not the address, so it is safe to
|
|
1226
|
+
* read — but note that a deployment with no SMTP (`mail:
|
|
1227
|
+
* 'not_configured'`) cannot complete a self-registration at all: unlike
|
|
1228
|
+
* an invitation, where a developer can hand the link over directly, the
|
|
1229
|
+
* confirmation link has no other channel.
|
|
1230
|
+
*
|
|
1231
|
+
* **The role is not yours to choose.** There is no `role` parameter: the
|
|
1232
|
+
* app's own `selfRegistration.role_id` decides it (§3.2 — a pool member
|
|
1233
|
+
* has exactly one role per app), and a signature that accepted one would
|
|
1234
|
+
* be offering a choice the platform has to refuse anyway.
|
|
1235
|
+
*
|
|
1236
|
+
* **Fails the same way whether self-registration is off or the address's
|
|
1237
|
+
* domain is not permitted.** Those two refusals are deliberately
|
|
1238
|
+
* indistinguishable, for the same reason as above: telling them apart
|
|
1239
|
+
* would let a caller probe configuration that is not theirs to see. Do
|
|
1240
|
+
* not build a UI that tries to tell "this app doesn't allow sign-ups"
|
|
1241
|
+
* apart from "your email domain isn't allowed" from the error alone.
|
|
1242
|
+
*
|
|
1243
|
+
* Behind the same limiter as `login` (`rate_limited`, W6c) — creating a
|
|
1244
|
+
* row is the cheapest attack this platform has to defend against, so
|
|
1245
|
+
* expect this to be throttled first under load, not last.
|
|
1246
|
+
*/
|
|
1247
|
+
register(email: string, password: string): Promise<ClientRegisterResponse>;
|
|
1248
|
+
/**
|
|
1249
|
+
* Spends a self-registration confirmation token and returns the new
|
|
1250
|
+
* session — the counterpart to `register` above. Stores the session
|
|
1251
|
+
* exactly like `login`, so a caller who has just confirmed is not then
|
|
1252
|
+
* told to log in separately.
|
|
1253
|
+
*
|
|
1254
|
+
* The token is single-use and expires; using it twice, or too late,
|
|
1255
|
+
* answers `token_spent` either way, same as `confirmPasswordReset` below.
|
|
1256
|
+
*/
|
|
1257
|
+
confirmRegistration(token: string): Promise<void>;
|
|
1258
|
+
/**
|
|
1259
|
+
* Changes the current end user's password (spec §3, W6c).
|
|
1260
|
+
*
|
|
1261
|
+
* `currentPassword` is required by the server even though the session
|
|
1262
|
+
* already proves identity — it is what stops a stolen *session* from
|
|
1263
|
+
* becoming a stolen *account* (see `passwordChangeRequest` in
|
|
1264
|
+
* `@fleetless/contracts`).
|
|
1265
|
+
*
|
|
1266
|
+
* **Every other session of this identity is revoked on success, and this
|
|
1267
|
+
* call's own session is re-issued, not left alone.** `passwordChangeRequest`
|
|
1268
|
+
* carries nothing that identifies the caller's own refresh family, so the
|
|
1269
|
+
* server cannot spare one token out of the family it just revoked — it
|
|
1270
|
+
* revokes all of them and hands back a fresh pair, which this method
|
|
1271
|
+
* stores exactly like `login` does. Skipping that store would leave the
|
|
1272
|
+
* caller holding tokens the server has already revoked, working only
|
|
1273
|
+
* until the access token expires and then silently logged out —
|
|
1274
|
+
* indistinguishable from the change having failed, which is the one
|
|
1275
|
+
* outcome this route exists to prevent. A user with other tabs or
|
|
1276
|
+
* devices logged in will see *those* signed out the moment this
|
|
1277
|
+
* resolves; if your app does not already make that consequence visible
|
|
1278
|
+
* before they confirm, they will find out from a support ticket instead
|
|
1279
|
+
* of from you.
|
|
1280
|
+
*/
|
|
1281
|
+
changePassword(currentPassword: string, newPassword: string): Promise<void>;
|
|
1282
|
+
/**
|
|
1283
|
+
* Requests a password-reset link for `email` (spec §3, W6c) — sent by
|
|
1284
|
+
* mail; the console-issued link is the primary path, this is the
|
|
1285
|
+
* self-service one.
|
|
1286
|
+
*
|
|
1287
|
+
* **Resolves the same way whether or not `email` belongs to an account,
|
|
1288
|
+
* and always did — this is not a bug to work around.** It is the one
|
|
1289
|
+
* unauthenticated route in this SDK where revealing existence would be an
|
|
1290
|
+
* account-enumeration oracle (see `passwordResetRequest`'s doc comment in
|
|
1291
|
+
* `@fleetless/contracts`). There is nothing in this method's return value
|
|
1292
|
+
* or its error codes that distinguishes "sent" from "no such account" —
|
|
1293
|
+
* do not build a UI branch for "no such account" here, because there is
|
|
1294
|
+
* nothing to branch on, on purpose.
|
|
1295
|
+
*/
|
|
1296
|
+
requestPasswordReset(email: string): Promise<void>;
|
|
1297
|
+
/**
|
|
1298
|
+
* Completes a password reset using the token from the emailed link (spec
|
|
1299
|
+
* §3, W6c).
|
|
1300
|
+
*
|
|
1301
|
+
* The token is single-use and expires; using it twice, or too late,
|
|
1302
|
+
* answers `token_spent` either way — deliberately one code for both (see
|
|
1303
|
+
* `ERROR_CODES` in `@fleetless/contracts`): telling them apart would tell
|
|
1304
|
+
* a stranger whether a token ever existed, and the recovery is the same
|
|
1305
|
+
* regardless — request a new link. Succeeding revokes every session of
|
|
1306
|
+
* that identity, the same as `changePassword` — a forgotten password is
|
|
1307
|
+
* one of the two states where somebody else may be holding a session.
|
|
1308
|
+
*/
|
|
1309
|
+
confirmPasswordReset(token: string, newPassword: string): Promise<void>;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* The snapshot's metadata alone, without the bytes (spec §10). All fields
|
|
1314
|
+
* are `null` together when nothing has been captured yet for this camera —
|
|
1315
|
+
* a fresh configuration before the first `snapshot_interval_ms` elapses, say
|
|
1316
|
+
* — which is a state, not a failure: the wire answers it with
|
|
1317
|
+
* `no_snapshot_yet`, and both `snapshot`/`snapshotMeta` absorb that code
|
|
1318
|
+
* here rather than throw it, so a caller checks `age_ms === null` instead of
|
|
1319
|
+
* wrapping every poll in a try/catch for something that is not exceptional.
|
|
1320
|
+
*
|
|
1321
|
+
* `age_ms` is **always** the cloud's own figure, never recomputed client-side
|
|
1322
|
+
* as `Date.now() - timestamp_ms`: the cloud is the one clock that knows how
|
|
1323
|
+
* long it has actually held the frame, and recomputing would reintroduce the
|
|
1324
|
+
* viewer's own clock skew as a source of lying about freshness (see
|
|
1325
|
+
* `SNAPSHOT_HEADERS`'s doc comment in `@fleetless/contracts`).
|
|
1326
|
+
*/
|
|
1327
|
+
interface CameraSnapshotMeta {
|
|
1328
|
+
mime: string | null;
|
|
1329
|
+
width: number | null;
|
|
1330
|
+
height: number | null;
|
|
1331
|
+
/** The bridge's capture time (§6.3). */
|
|
1332
|
+
timestamp_ms: number | null;
|
|
1333
|
+
age_ms: number | null;
|
|
1334
|
+
}
|
|
1335
|
+
/** One snapshot read: the image bytes plus everything needed to state how old they are (spec §10). */
|
|
1336
|
+
interface CameraSnapshot extends CameraSnapshotMeta {
|
|
1337
|
+
image: Uint8Array | null;
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* What a LiveKit client needs to join, plus the means to leave (spec §10,
|
|
1341
|
+
* §14.3: "Kamera (Snapshot-URL + LiveKit-Track-Handle)"). Hand `url`/`token`
|
|
1342
|
+
* straight to a LiveKit client SDK (e.g. `Room.connect(url, token)`) — this
|
|
1343
|
+
* SDK stops there on purpose: no video widget, no teleop-style helper
|
|
1344
|
+
* (§14.3 keeps it thin).
|
|
1345
|
+
*/
|
|
1346
|
+
interface CameraLiveSession {
|
|
1347
|
+
/**
|
|
1348
|
+
* This viewer's own hold (W6b) — what `release()` releases, and the only
|
|
1349
|
+
* thing distinguishing this session from every other tab of the same
|
|
1350
|
+
* identity watching the same camera. Not previously addressable: a `DELETE`
|
|
1351
|
+
* with no id released **all** of this identity's holds on the slug, so one
|
|
1352
|
+
* tab closing stopped the robot for every other tab too. See `release()`'s
|
|
1353
|
+
* doc comment for what changed and what did not.
|
|
1354
|
+
*/
|
|
1355
|
+
session_id: string;
|
|
1356
|
+
url: string;
|
|
1357
|
+
room: string;
|
|
1358
|
+
token: string;
|
|
1359
|
+
/**
|
|
1360
|
+
* When this token can no longer be used to **join** — not when an
|
|
1361
|
+
* already-joined session ends. LiveKit checks a token at connect time
|
|
1362
|
+
* only, so a `Room` that joined before this timestamp keeps streaming
|
|
1363
|
+
* past it untouched; this field bounds how long an unused token sits
|
|
1364
|
+
* around, nothing more.
|
|
1365
|
+
*
|
|
1366
|
+
* It is **not** a backstop for a forgotten `release()`, a crash, or a
|
|
1367
|
+
* `kill -9` after joining: what ends an already-joined session is
|
|
1368
|
+
* `release()` plus disconnecting the `Room`, the cloud noticing (via its
|
|
1369
|
+
* own reconciliation against LiveKit's actual room participants) that
|
|
1370
|
+
* this viewer is gone, or revocation kicking the participant outright.
|
|
1371
|
+
* Do not design around `expires_at` as if it were any of those.
|
|
1372
|
+
*/
|
|
1373
|
+
expires_at: string;
|
|
1374
|
+
/**
|
|
1375
|
+
* Tells the cloud this viewer no longer wants to hold the camera live —
|
|
1376
|
+
* **this** hold, addressed by `session_id` (W6b), and no other tab's.
|
|
1377
|
+
*
|
|
1378
|
+
* Before W6b, `DELETE` carried no id and released every hold this identity
|
|
1379
|
+
* had on the slug — so one tab's `release()` (or its unmount cleanup)
|
|
1380
|
+
* stopped the robot out from under every other tab of the same logged-in
|
|
1381
|
+
* user, which kept rendering a frozen frame because a LiveKit token is
|
|
1382
|
+
* checked at join and never again. Each `CameraLiveSession` now releases
|
|
1383
|
+
* only the hold it itself took.
|
|
1384
|
+
*
|
|
1385
|
+
* **This alone does not stop the stream.** The cloud makes LiveKit room
|
|
1386
|
+
* participation the authoritative refcount, not this call — precisely
|
|
1387
|
+
* because a closing tab cannot be relied on to make it. `release()` is a
|
|
1388
|
+
* courteous fast path; the robot actually stops publishing once every
|
|
1389
|
+
* viewer's LiveKit `Room` has disconnected, which the SFU notices on its
|
|
1390
|
+
* own with no cooperation required. **Always pair this with disconnecting
|
|
1391
|
+
* the `Room` you connected with `url`/`token`** — see the README's
|
|
1392
|
+
* Cameras section for the paired cleanup pattern; a `release()` that ran
|
|
1393
|
+
* alone while the `Room` stayed connected would stop nothing.
|
|
1394
|
+
*
|
|
1395
|
+
* Safe to call more than once (only the first call does anything) and
|
|
1396
|
+
* never rejects — this is a courtesy notification, not the thing that
|
|
1397
|
+
* actually stops the stream (see above), so there is nothing a caller
|
|
1398
|
+
* could usefully do with a rejection here. That also makes this safe to
|
|
1399
|
+
* use directly as e.g. a React effect's cleanup return value, including
|
|
1400
|
+
* from `beforeunload`, where a call that could throw would be a liability.
|
|
1401
|
+
*
|
|
1402
|
+
* **A failed DELETE here is not observable anywhere** — not as a
|
|
1403
|
+
* rejection, not as a realtime event, not as a field on this object. This
|
|
1404
|
+
* is a deliberate decision, not an oversight (W6b review): the only
|
|
1405
|
+
* consumer of that information would be code deciding whether to retry,
|
|
1406
|
+
* and the backstop this comment already describes — the cloud's own
|
|
1407
|
+
* LiveKit-participation reconciliation — makes a retry unnecessary for
|
|
1408
|
+
* correctness. If a future caller needs to know "did my release actually
|
|
1409
|
+
* reach the cloud" (telemetry, say), that is a new, additive signal to
|
|
1410
|
+
* design, not a change to this method's contract.
|
|
1411
|
+
*/
|
|
1412
|
+
release(): Promise<void>;
|
|
1413
|
+
}
|
|
1414
|
+
interface CamerasApi {
|
|
1415
|
+
/** Every camera exposed on this robot (spec §11.2's per-robot descriptor list, extended to the camera kind). */
|
|
1416
|
+
list(robotId: string): Promise<CameraDescriptor[]>;
|
|
1417
|
+
/**
|
|
1418
|
+
* The current snapshot: image bytes plus its age. Independent of `live` —
|
|
1419
|
+
* a snapshot keeps updating on `snapshot_interval_ms` whether or not
|
|
1420
|
+
* anyone is watching live (§10), and keeps being served, with a growing
|
|
1421
|
+
* age, even while the bridge is offline.
|
|
1422
|
+
*/
|
|
1423
|
+
snapshot(robotId: string, slug: string): Promise<CameraSnapshot>;
|
|
1424
|
+
/**
|
|
1425
|
+
* The snapshot's metadata alone — for polling "is there a newer frame
|
|
1426
|
+
* yet?" without re-downloading the image on every check. Prefer this over
|
|
1427
|
+
* `snapshot` for a view that only needs to show an age (e.g. "updated 2s
|
|
1428
|
+
* ago") and fetches pixels on demand.
|
|
1429
|
+
*/
|
|
1430
|
+
snapshotMeta(robotId: string, slug: string): Promise<CameraSnapshotMeta>;
|
|
1431
|
+
/**
|
|
1432
|
+
* Takes a refcounted hold on this camera's live stream (spec §10): the
|
|
1433
|
+
* first `live()` on a slug starts the robot publishing, the last viewer
|
|
1434
|
+
* leaving stops it. Deliberately not deduplicated locally across multiple
|
|
1435
|
+
* `live()` calls for the same `(robotId, slug)` — unlike a datapoint
|
|
1436
|
+
* subscription, each call needs its own distinct LiveKit participant, so
|
|
1437
|
+
* a local counter here would just be the same shared-count bug
|
|
1438
|
+
* `slug-subscriptions.ts` fixed, self-inflicted on a resource the cloud
|
|
1439
|
+
* already counts correctly.
|
|
1440
|
+
*/
|
|
1441
|
+
live(robotId: string, slug: string): Promise<CameraLiveSession>;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
interface DatapointSubscriptionHandlers {
|
|
1445
|
+
/** Called with the current value on subscribe, then again on every change. */
|
|
1446
|
+
onEvent(event: DatapointEvent): void;
|
|
1447
|
+
/** Called once if the subscription is refused (§11.5: e.g. `forbidden`, `unknown_datapoint`). */
|
|
1448
|
+
onError?(error: FleetlessError): void;
|
|
1449
|
+
}
|
|
1450
|
+
interface DatapointSubscription {
|
|
1451
|
+
/** Stops the subscription and, if the channel is currently connected, tells the server. */
|
|
1452
|
+
unsubscribe(): void;
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Window aggregation for `history` (spec §8). `window` and `agg` always
|
|
1456
|
+
* travel together on the wire — the cloud refuses one without the other
|
|
1457
|
+
* rather than defaulting either, since a silently chosen aggregation is a
|
|
1458
|
+
* chart that lies quietly — so they live in one object here instead of two
|
|
1459
|
+
* independent optional fields a caller could set only one of. The same
|
|
1460
|
+
* reasoning as `cameraSource` being a discriminated union rather than
|
|
1461
|
+
* optional fields: make the impossible combination unrepresentable, not
|
|
1462
|
+
* merely rejected.
|
|
1463
|
+
*/
|
|
1464
|
+
interface HistoryAggregation {
|
|
1465
|
+
/** Bucket width, e.g. `10s`, `1m`. */
|
|
1466
|
+
window: string;
|
|
1467
|
+
agg: 'min' | 'max' | 'avg';
|
|
1468
|
+
/** A numeric field inside an object value, e.g. `pose.x` (§4.4 paths). Only meaningful when the datapoint's own value is not itself a number. */
|
|
1469
|
+
field?: string;
|
|
1470
|
+
}
|
|
1471
|
+
interface HistoryOptions {
|
|
1472
|
+
/**
|
|
1473
|
+
* `now-30s` / `now-5m` / `now-1h`, or absolute unix milliseconds — as a
|
|
1474
|
+
* **string** either way, exactly as the wire query expects it. The SDK
|
|
1475
|
+
* does not accept a `Date` or a `number` and stringify it for you: that
|
|
1476
|
+
* would be a convenience that quietly decides which of the two forms you
|
|
1477
|
+
* meant, and the next person reading the wire traffic would not know
|
|
1478
|
+
* which of us made that call.
|
|
1479
|
+
*/
|
|
1480
|
+
from: string;
|
|
1481
|
+
/** Same two forms as `from`. Defaults to now. */
|
|
1482
|
+
to?: string;
|
|
1483
|
+
limit?: number;
|
|
1484
|
+
/** Present: the result is aggregated buckets. Absent: raw samples. */
|
|
1485
|
+
aggregate?: HistoryAggregation;
|
|
1486
|
+
}
|
|
1487
|
+
interface DatapointsApi {
|
|
1488
|
+
get(robotId: string, slug: string): Promise<DatapointValue>;
|
|
1489
|
+
/**
|
|
1490
|
+
* Subscribes over the realtime channel. Reconnect and re-authentication
|
|
1491
|
+
* are handled by the shared `RealtimeChannel`; this resends its
|
|
1492
|
+
* `subscribe` frame after every (re)connect, so a network drop is
|
|
1493
|
+
* invisible to the caller beyond a gap in events.
|
|
1494
|
+
*
|
|
1495
|
+
* Reference-counted per `(robotId, slug)`: two subscriptions to the same
|
|
1496
|
+
* pair share one wire subscription. Unsubscribing one never affects the
|
|
1497
|
+
* other — the `unsubscribe` frame is sent only when the last subscriber
|
|
1498
|
+
* on that pair goes away. This matters in practice: two widgets showing
|
|
1499
|
+
* the same battery value, or a component mounted twice under React
|
|
1500
|
+
* StrictMode, both subscribe to the same key. The count itself lives in
|
|
1501
|
+
* `slug-subscriptions.ts`, shared with `actions.subscribe`/`services.call`
|
|
1502
|
+
* — a slug is one namespace across kinds, and so is its subscription.
|
|
1503
|
+
*/
|
|
1504
|
+
subscribe(robotId: string, slug: string, handlers: DatapointSubscriptionHandlers): DatapointSubscription;
|
|
1505
|
+
/**
|
|
1506
|
+
* Reads recorded history for a `retention: true` datapoint (spec §8) over
|
|
1507
|
+
* REST — no realtime channel involved, the same way `cameras.snapshot`
|
|
1508
|
+
* isn't. Returns a **discriminated result**: passing `aggregate` gets you
|
|
1509
|
+
* back `HistoryBucketsResponse` (`kind: 'buckets'`), leaving it out gets
|
|
1510
|
+
* you `HistorySamplesResponse` (`kind: 'samples'`) — two overloads so a
|
|
1511
|
+
* caller who already knows which one they asked for isn't forced to
|
|
1512
|
+
* narrow something they determined themselves. `kind` still carries the
|
|
1513
|
+
* same information on both, so code that holds the result dynamically
|
|
1514
|
+
* (e.g. read from a variable typed as the union) can still branch on it.
|
|
1515
|
+
*
|
|
1516
|
+
* **Rejects, does not silently empty out, two specific refusals** —
|
|
1517
|
+
* unlike `cameras.snapshot`'s absorption of `no_snapshot_yet` into a null
|
|
1518
|
+
* read, these two must reach the caller as thrown `FleetlessError`s:
|
|
1519
|
+
* - `not_recorded` — the slug exists and is granted, but is configured
|
|
1520
|
+
* live-only. An empty result here would look exactly like "recorded,
|
|
1521
|
+
* but nothing in this window", and the two need opposite fixes: turn
|
|
1522
|
+
* recording on, versus look at a different range.
|
|
1523
|
+
* - `not_aggregatable` — `aggregate` was given for a value that isn't a
|
|
1524
|
+
* number and no numeric `aggregate.field` was named.
|
|
1525
|
+
*/
|
|
1526
|
+
history(robotId: string, slug: string, options: HistoryOptions & {
|
|
1527
|
+
aggregate: HistoryAggregation;
|
|
1528
|
+
}): Promise<HistoryBucketsResponse>;
|
|
1529
|
+
history(robotId: string, slug: string, options: HistoryOptions & {
|
|
1530
|
+
aggregate?: undefined;
|
|
1531
|
+
}): Promise<HistorySamplesResponse>;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
interface JobsApi {
|
|
1535
|
+
/**
|
|
1536
|
+
* Every job the platform currently believes this robot has — `GET
|
|
1537
|
+
* /api/robots/:id/jobs` (W6b, contracts `robotJobsResponse` doc comment).
|
|
1538
|
+
*
|
|
1539
|
+
* `actions.subscribe`/`services.call` and `GET /jobs/:slug` (the per-slug
|
|
1540
|
+
* route those build on) all require already knowing the slug. That is not
|
|
1541
|
+
* always true: a reconnecting bridge can name a job the cloud only
|
|
1542
|
+
* *adopted*, and a configuration change can leave a job on a slug the
|
|
1543
|
+
* published document no longer contains. Both are jobs no slug can name,
|
|
1544
|
+
* which is exactly what this method is for (register row 2k) — an app
|
|
1545
|
+
* developer had no way to reach them before this.
|
|
1546
|
+
*
|
|
1547
|
+
* At most one entry per slug: the current job there, exactly what a
|
|
1548
|
+
* per-slug read would answer for that slug. Not a history endpoint.
|
|
1549
|
+
* Grant-filtered same as `cameras.list`/`datapoints` — an end user or
|
|
1550
|
+
* server key sees only jobs on slugs their role grants; a developer
|
|
1551
|
+
* session sees every job on the robot. Never empty-vs-missing ambiguity:
|
|
1552
|
+
* a robot doing nothing resolves `[]`.
|
|
1553
|
+
*
|
|
1554
|
+
* **Ordered newest first by `started_at`, with `job.seq` as the
|
|
1555
|
+
* tiebreaker** (`started_at` alone is not a total order — two jobs minted
|
|
1556
|
+
* in the same millisecond used to sort arbitrarily, differently on each
|
|
1557
|
+
* query). But for an **adopted** job, `started_at` is adoption time, not
|
|
1558
|
+
* when it actually started on the robot — the cloud only learns of it at
|
|
1559
|
+
* `hello`, having never minted it, and has no other honest value to put
|
|
1560
|
+
* there. So this is newest-*known*-first: a job the robot has been
|
|
1561
|
+
* running for an hour can sit above one started a minute ago, if the
|
|
1562
|
+
* hour-long one was only just adopted (contracts `robotJobsResponse` doc
|
|
1563
|
+
* comment).
|
|
1564
|
+
*/
|
|
1565
|
+
list(robotId: string): Promise<Job[]>;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
interface PublishersApi {
|
|
1569
|
+
/**
|
|
1570
|
+
* Publishes one message to a publisher (spec §4.2, §6.4).
|
|
1571
|
+
*
|
|
1572
|
+
* This is a plain method call — there is deliberately no deadman switch,
|
|
1573
|
+
* rate governor or "takt" helper here (spec §14.3). The bridge's own
|
|
1574
|
+
* `timeout_ms` failsafe is the platform's safety primitive: if messages
|
|
1575
|
+
* stop arriving — including because this process crashed — the bridge
|
|
1576
|
+
* publishes the configured failsafe message itself. That does **not**
|
|
1577
|
+
* mean the SDK protects a caller who stops calling `publish` on purpose
|
|
1578
|
+
* without stopping cleanly (e.g. no repeated call at a safe rate): the
|
|
1579
|
+
* safety pattern for *how often* and *when* to publish belongs in the
|
|
1580
|
+
* app, not here. See the README's "No teleop helpers" section before
|
|
1581
|
+
* building a publisher-driven control loop.
|
|
1582
|
+
*
|
|
1583
|
+
* Rejects `publisher_busy` while a different user is publishing and has
|
|
1584
|
+
* not been quiet for `quiet_timeout_ms` yet (§6.4) — whoever publishes
|
|
1585
|
+
* holds the publisher implicitly exclusive.
|
|
1586
|
+
*/
|
|
1587
|
+
publish(robotId: string, slug: string, message: Record<string, unknown>, options?: SendCommandOptions): Promise<void>;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
interface ServicesApi {
|
|
1591
|
+
/**
|
|
1592
|
+
* Calls a service and resolves with its result (spec §4.2, §11.3). A
|
|
1593
|
+
* service call is a job underneath — the same `job_id` exchange and
|
|
1594
|
+
* disconnect survival as an action (§6.1: "an action goal or a service
|
|
1595
|
+
* call") — but that is deliberately invisible here: the caller gets a
|
|
1596
|
+
* plain `Promise<result>`, matching the REST `serviceCallResponse` shape's
|
|
1597
|
+
* developer experience. There is nothing to subscribe to for a service —
|
|
1598
|
+
* no feedback, no progress, no cancel — so this call already waits for
|
|
1599
|
+
* the terminal state internally.
|
|
1600
|
+
*
|
|
1601
|
+
* `options.patienceMs` bounds the **whole wait** for a service call (W6b)
|
|
1602
|
+
* — unlike an action, where it bounds acceptance only — because a service
|
|
1603
|
+
* has no further state to observe once it settles; the platform gives up
|
|
1604
|
+
* on the ROS call itself after this long.
|
|
1605
|
+
*
|
|
1606
|
+
* `options.timeoutMs` bounds this SDK's own local wait for the WHOLE
|
|
1607
|
+
* call — the ack that a job was created, plus however much of the
|
|
1608
|
+
* budget is left for it to then reach a terminal state (D10) — not two
|
|
1609
|
+
* separate `timeoutMs`-length windows back to back. A caller who sets
|
|
1610
|
+
* `timeoutMs: 5000` is bounding total latency at ~5s, not ~10s; the
|
|
1611
|
+
* number means what it says, once, for the whole call.
|
|
1612
|
+
*
|
|
1613
|
+
* It is also **not independent** of `patienceMs` (D3a): left unset, it
|
|
1614
|
+
* is derived from `patienceMs` so this SDK's local clock cannot fire
|
|
1615
|
+
* before the platform's own deadline has even been reached. Setting
|
|
1616
|
+
* both, with `timeoutMs` shorter than `patienceMs`, throws
|
|
1617
|
+
* `invalid_option` synchronously rather than letting the two race — see
|
|
1618
|
+
* `resolveLocalWaitMs` in `commands.ts` for the full reasoning.
|
|
1619
|
+
*/
|
|
1620
|
+
call(robotId: string, slug: string, params: Record<string, unknown>, options?: InvokeOptions): Promise<unknown>;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
interface FleetlessClientOptions {
|
|
1624
|
+
/** Base URL of the Fleetless REST API, e.g. `https://api.fleetless.dev`. */
|
|
1625
|
+
apiUrl: string;
|
|
1626
|
+
/** The app's identifier (the slug shown in the console), sent on every login. */
|
|
1627
|
+
appIdentifier: string;
|
|
1628
|
+
/**
|
|
1629
|
+
* Where refresh/access tokens are kept between calls. Defaults to an
|
|
1630
|
+
* in-memory store — pass your own (localStorage, a cookie, a native
|
|
1631
|
+
* keystore) to persist a session across reloads. The SDK never assumes a
|
|
1632
|
+
* browser exists.
|
|
1633
|
+
*/
|
|
1634
|
+
tokenStore?: TokenStore;
|
|
1635
|
+
/**
|
|
1636
|
+
* A server key (`flk_...`) for server-side callers with full app rights.
|
|
1637
|
+
* Mutually exclusive with `tokenStore`-based login: a client constructed
|
|
1638
|
+
* with a server key never calls `auth.login`/`auth.logout`.
|
|
1639
|
+
*/
|
|
1640
|
+
serverKey?: string;
|
|
1641
|
+
/** Injectable for tests, or a non-global `fetch` implementation. */
|
|
1642
|
+
fetch?: typeof fetch;
|
|
1643
|
+
/** Injectable for tests, or a non-global `WebSocket` implementation. */
|
|
1644
|
+
WebSocket?: typeof WebSocket;
|
|
1645
|
+
/** Defaults to `apiUrl` with http(s) swapped for ws(s) and `/realtime` appended. */
|
|
1646
|
+
realtimeUrl?: string;
|
|
1647
|
+
}
|
|
1648
|
+
interface FleetlessClientConfig {
|
|
1649
|
+
readonly apiUrl: string;
|
|
1650
|
+
readonly appIdentifier: string;
|
|
1651
|
+
readonly realtimeUrl: string;
|
|
1652
|
+
}
|
|
1653
|
+
interface FleetlessClient {
|
|
1654
|
+
readonly config: FleetlessClientConfig;
|
|
1655
|
+
readonly auth: AuthApi;
|
|
1656
|
+
readonly datapoints: DatapointsApi;
|
|
1657
|
+
readonly actions: ActionsApi;
|
|
1658
|
+
readonly services: ServicesApi;
|
|
1659
|
+
readonly publishers: PublishersApi;
|
|
1660
|
+
readonly cameras: CamerasApi;
|
|
1661
|
+
/**
|
|
1662
|
+
* Robot-wide job reads that do not fit under `actions`/`services` because
|
|
1663
|
+
* they are not addressed by slug — see `JobsApi.list`.
|
|
1664
|
+
*/
|
|
1665
|
+
readonly jobs: JobsApi;
|
|
1666
|
+
/** URDF + mesh reads (spec §4.6) — list/get/urdf, plus the `urdf-loader` mesh callback. */
|
|
1667
|
+
readonly assets: AssetsApi;
|
|
1668
|
+
/**
|
|
1669
|
+
* Closes the realtime channel and stops it from reconnecting. Safe to
|
|
1670
|
+
* call whether or not any subscription was ever made, and safe to call
|
|
1671
|
+
* more than once. A Node script (the exact use case `serverKey` is for)
|
|
1672
|
+
* that never calls this after subscribing will not exit on its own — an
|
|
1673
|
+
* open WebSocket keeps the event loop alive. `auth.logout()` calls this
|
|
1674
|
+
* automatically; call it yourself too if the process should exit without
|
|
1675
|
+
* logging out (e.g. a server-side caller shutting down).
|
|
1676
|
+
*/
|
|
1677
|
+
close(): void;
|
|
1678
|
+
}
|
|
1679
|
+
declare function createClient(options: FleetlessClientOptions): FleetlessClient;
|
|
1680
|
+
|
|
1681
|
+
export { type ActionsApi, type Asset, type AssetBytes, type AssetListResponse, type AssetsApi, type AuthApi, type BeginHostedLoginOptions, type BusyDetails, type CameraDescriptor, type CameraLiveSession, type CameraSnapshot, type CameraSnapshotMeta, type CamerasApi, type ClientIdentity, type ClientRegisterResponse, type CompleteHostedLoginOptions, type CreateMeshLoaderOptions, type DatapointEvent, type DatapointSubscription, type DatapointSubscriptionHandlers, type DatapointValue, type DatapointsApi, type FleetlessClient, type FleetlessClientConfig, type FleetlessClientOptions, FleetlessError, type FleetlessErrorCode, type FleetlessErrorOptions, type HistoryAggregation, type HistoryBucketsResponse, type HistoryOptions, type HistorySamplesResponse, type HostedLoginRequest, InMemoryTokenStore, type Job, type JobEvent, type JobState, type JobSubscription, type JobSubscriptionHandlers, type JobsApi, type MailStatus, type MeshLoaderDelegate, type ParameterInvalidDetails, type ParameterViolation, type PrepareUrdfSceneOptions, type PublishersApi, type RateLimitDetails, SDK_ERROR_CODES, type SdkErrorCode, type SendCommandOptions, type ServicesApi, type StoredSession, type TokenStore, type UrdfCompleteness, type UrdfSceneManager, type UrdfSceneResources, createClient, parameterInvalidDetails };
|