@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/dist/index.cjs ADDED
@@ -0,0 +1,3662 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ FleetlessError: () => FleetlessError,
24
+ InMemoryTokenStore: () => InMemoryTokenStore,
25
+ SDK_ERROR_CODES: () => SDK_ERROR_CODES,
26
+ createClient: () => createClient,
27
+ parameterInvalidDetails: () => parameterInvalidDetails
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/errors.ts
32
+ var SDK_ERROR_CODES = [
33
+ "no_session",
34
+ "no_websocket",
35
+ "unparseable_error",
36
+ "command_timeout",
37
+ "command_outcome_unknown",
38
+ "unexpected_response",
39
+ "invalid_option",
40
+ "untrusted_absolute_url",
41
+ "state_mismatch",
42
+ "no_hosted_login_attempt",
43
+ "no_urdf_synced",
44
+ "aborted"
45
+ ];
46
+ var FleetlessError = class extends Error {
47
+ code;
48
+ details;
49
+ status;
50
+ constructor(code, message, options = {}) {
51
+ super(message);
52
+ this.name = "FleetlessError";
53
+ this.code = code;
54
+ this.details = options.details;
55
+ this.status = options.status;
56
+ }
57
+ };
58
+
59
+ // src/actions.ts
60
+ function createActionsApi(transport, jobSubscriptions) {
61
+ return {
62
+ async invoke(robotId, slug2, params, options) {
63
+ const result = await transport.invoke(robotId, slug2, params, options);
64
+ if (!result.job) {
65
+ throw new FleetlessError("unexpected_response", `The server accepted the invoke for '${slug2}' but returned no job to track.`);
66
+ }
67
+ return result.job;
68
+ },
69
+ async cancel(robotId, slug2, jobId, options) {
70
+ const result = await transport.cancel(robotId, slug2, jobId, options);
71
+ return result.job;
72
+ },
73
+ subscribe(robotId, slug2, handlers) {
74
+ return jobSubscriptions.subscribe(robotId, slug2, handlers);
75
+ }
76
+ };
77
+ }
78
+
79
+ // src/http.ts
80
+ var noCredentials = {
81
+ async token() {
82
+ return null;
83
+ },
84
+ async handleExpired() {
85
+ return false;
86
+ }
87
+ };
88
+ function mimeFromContentType(headers) {
89
+ const raw = headers.get("content-type");
90
+ if (raw === null) return null;
91
+ return raw.split(";")[0]?.trim() || null;
92
+ }
93
+ function pathSegment(value) {
94
+ return encodeURIComponent(value);
95
+ }
96
+ var HttpClient = class {
97
+ #baseUrl;
98
+ #fetch;
99
+ #credentials;
100
+ constructor(options) {
101
+ this.#baseUrl = options.baseUrl;
102
+ this.#fetch = options.fetch;
103
+ this.#credentials = options.credentials;
104
+ }
105
+ /**
106
+ * Swaps the credential source after construction. Exists to break a
107
+ * construction cycle: a session's `CredentialSource` needs this
108
+ * `HttpClient` to call `/api/client/refresh`, so the client is built
109
+ * first with a placeholder and the real source attached once it exists.
110
+ */
111
+ setCredentials(credentials) {
112
+ this.#credentials = credentials;
113
+ }
114
+ /**
115
+ * D8 (W6c): calling `request()` with one of `RequestBodyByRoute`'s
116
+ * literal paths requires `options.body` to match that route's contract
117
+ * type exactly, via `RequestOptionsFor<P>` below.
118
+ *
119
+ * **Deliberately not a pair of overloads** — a first version was:
120
+ * `request<T, P extends keyof RequestBodyByRoute>(path: P, options: {
121
+ * body: RequestBodyByRoute[P] } & ...): Promise<T>` followed by a looser
122
+ * `request<T>(path: string, options?: RequestOptions): Promise<T>`
123
+ * fallback. Verified wrong before it ever landed: TypeScript overload
124
+ * resolution tries each signature in order and silently falls through to
125
+ * the next one on a mismatch — so a call with the WRONG body for a mapped
126
+ * route simply failed to match the strict overload and matched the loose
127
+ * one instead, with **no diagnostic at all**. A single generic signature
128
+ * whose `options` type is *computed* from `P` (below) has no looser
129
+ * sibling to fall back to, so a mismatch has nowhere to go but an error.
130
+ *
131
+ * **`options` has no default value, on purpose.** A default of `{} as
132
+ * RequestOptionsFor<P>` would type-check by construction — the cast
133
+ * bypasses the very check this exists to add, which is the identical
134
+ * "special case that quietly exempts calls from the general rule" shape
135
+ * D7 already found once in this file. Every current call site to a route
136
+ * with no body already passes `{}` explicitly for exactly this reason.
137
+ */
138
+ async request(path, options) {
139
+ const response = await this.#send(path, options, true);
140
+ const text = await response.text();
141
+ if (text.length === 0) {
142
+ if (options.expectEmptyBody) return void 0;
143
+ throw new FleetlessError(
144
+ "unexpected_response",
145
+ `${options.method ?? "GET"} ${path} answered ok (${response.status}) but the body was empty; this route is expected to always return one.`
146
+ );
147
+ }
148
+ return JSON.parse(text);
149
+ }
150
+ /**
151
+ * Like `request`, but for an endpoint that answers with raw bytes instead
152
+ * of a JSON body (a camera snapshot, W5) — same auth attachment, same
153
+ * single retry on `token_expired`, same `FleetlessError` on a non-2xx
154
+ * response; only what a *successful* response is made of differs, so both
155
+ * methods share `#send` rather than duplicating that logic.
156
+ */
157
+ async requestBinary(path, options = {}) {
158
+ const response = await this.#send(path, options, true);
159
+ const body = new Uint8Array(await response.arrayBuffer());
160
+ return { body, headers: response.headers };
161
+ }
162
+ /**
163
+ * The base URL this client was constructed with — exposed so a caller
164
+ * building a URL that is not itself a `request()`/`requestBinary()` call
165
+ * (today: `auth.beginHostedLogin`'s `/oauth/authorize` link) does not need
166
+ * its own copy of the value `HttpClient` already holds.
167
+ */
168
+ get baseUrl() {
169
+ return this.#baseUrl;
170
+ }
171
+ /**
172
+ * `/oauth/token` (RFC 6749 §3.2, §5), and nothing else — deliberately not
173
+ * routed through `request()`/`#send()`, because every difference from
174
+ * those follows from one fact: this is not our own API.
175
+ *
176
+ * - **`application/x-www-form-urlencoded`, never JSON.** A real
177
+ * RFC-compliant client (gate step 4's `mcp-inspector`) sends form
178
+ * fields, not a JSON object — this is not a stylistic choice either side
179
+ * of this wire gets to make.
180
+ * - **Unauthenticated.** `/oauth/token` is a public client's endpoint (no
181
+ * `client_secret` — OAuth 2.1 + PKCE is the defence, same as
182
+ * `dynamicClientRegistrationResponse`'s `token_endpoint_auth_method:
183
+ * 'none'`), so there is no bearer credential to attach and no
184
+ * `token_expired` retry to apply — there is no token yet to expire.
185
+ * - **A different error dialect.** `oauth.ts`'s own doc comment says the
186
+ * OAuth/`apiError` split is "by audience, not by accident"; folding this
187
+ * into `request()`'s error handling would be the identical mistake one
188
+ * level down. A failure here answers `{ error, error_description }`
189
+ * (RFC 6749 §5.2), never `{ code, message }`. `error` becomes this
190
+ * `FleetlessError`'s `.code` directly — an OAuth error code (e.g.
191
+ * `invalid_grant`) IS the stable, branch-on value here, exactly as
192
+ * `apiError`'s `code` is for every other route.
193
+ * - **Exactly one `fetch()`, always.** No retry, no timeout-and-resend, no
194
+ * `signal` parameter at all — unlike `request()`/`requestBinary()`,
195
+ * nothing here can leave a second request in flight behind the
196
+ * caller's back. That matters specifically for the authorization-code
197
+ * grant: a second presentation of the same `code` revokes the whole
198
+ * token family server-side (André, 2026-08-18), so a caller-level retry
199
+ * racing an in-flight `completeHostedLogin()` is a real hazard this
200
+ * method cannot protect against on its own — see that method's own doc
201
+ * comment for the consequence.
202
+ */
203
+ async requestOAuth(path, formBody) {
204
+ const url = `${this.#baseUrl}${path}`;
205
+ const response = await this.#fetch(url, {
206
+ method: "POST",
207
+ headers: { "content-type": "application/x-www-form-urlencoded" },
208
+ body: new URLSearchParams(formBody).toString()
209
+ });
210
+ const text = await response.text();
211
+ if (response.ok) {
212
+ if (text.length === 0) {
213
+ throw new FleetlessError("unexpected_response", `POST ${path} answered ok (${response.status}) but the body was empty.`);
214
+ }
215
+ return JSON.parse(text);
216
+ }
217
+ const body = safeJsonParse(text);
218
+ const code = typeof body?.error === "string" ? body.error : "unparseable_error";
219
+ const message = typeof body?.error_description === "string" ? body.error_description : response.statusText || "OAuth request failed";
220
+ throw new FleetlessError(code, message, { status: response.status });
221
+ }
222
+ /** Fetches with credentials attached and the token_expired retry applied; returns the raw successful `Response`, or throws `FleetlessError`. */
223
+ async #send(path, options, allowRetry) {
224
+ const isAbsolute = /^https?:\/\//.test(path);
225
+ const url = isAbsolute ? path : `${this.#baseUrl}${path}`;
226
+ if (isAbsolute && new URL(url).origin !== new URL(this.#baseUrl).origin) {
227
+ throw new FleetlessError(
228
+ "untrusted_absolute_url",
229
+ `Refusing to fetch '${url}': its origin does not match this client's own API (${this.#baseUrl}). This SDK only attaches credentials to requests aimed at the API it was configured with.`
230
+ );
231
+ }
232
+ const headers = {};
233
+ if (options.body !== void 0) headers["content-type"] = "application/json";
234
+ if (!options.skipAuth) {
235
+ const token = await this.#credentials.token();
236
+ if (token) headers.authorization = `Bearer ${token}`;
237
+ }
238
+ const response = await this.#fetch(url, {
239
+ method: options.method ?? "GET",
240
+ headers,
241
+ body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
242
+ signal: options.signal
243
+ });
244
+ if (response.ok) return response;
245
+ const body = await safeJson(response);
246
+ const code = typeof body?.code === "string" ? body.code : "unparseable_error";
247
+ const message = typeof body?.message === "string" ? body.message : response.statusText || "Request failed";
248
+ if (!options.skipAuth && allowRetry && code === "token_expired") {
249
+ let canRetry;
250
+ try {
251
+ canRetry = await this.#credentials.handleExpired();
252
+ } catch (refreshError) {
253
+ if (refreshError instanceof FleetlessError) throw refreshError;
254
+ throw new FleetlessError(code, message, { details: body?.details, status: response.status });
255
+ }
256
+ if (canRetry) return this.#send(path, options, false);
257
+ }
258
+ throw new FleetlessError(code, message, { details: body?.details, status: response.status });
259
+ }
260
+ };
261
+ async function safeJson(response) {
262
+ try {
263
+ return await response.json();
264
+ } catch {
265
+ return void 0;
266
+ }
267
+ }
268
+ function safeJsonParse(text) {
269
+ try {
270
+ return JSON.parse(text);
271
+ } catch {
272
+ return void 0;
273
+ }
274
+ }
275
+
276
+ // src/assets.ts
277
+ var DEFAULT_MESH_LOADER_TIMEOUT_MS = 3e4;
278
+ var DEFAULT_SCENE_LOAD_CONCURRENCY = 6;
279
+ var managersWithPreparedUrdfScene = /* @__PURE__ */ new WeakSet();
280
+ function urlModifierKeysFor(name) {
281
+ const prefix = "package://";
282
+ if (!name.startsWith(prefix)) return [name];
283
+ return [name, `/${name.slice(prefix.length)}`];
284
+ }
285
+ function normalizedUrlModifierKey(url) {
286
+ if (url.startsWith("package://") || /^[a-z][a-z0-9+.-]*:/i.test(url)) return url;
287
+ try {
288
+ return new URL(url, "file:///").pathname;
289
+ } catch {
290
+ return url;
291
+ }
292
+ }
293
+ function packageNameOf(name) {
294
+ const prefix = "package://";
295
+ if (!name.startsWith(prefix)) return null;
296
+ const rest = name.slice(prefix.length);
297
+ const slash = rest.indexOf("/");
298
+ return slash === -1 ? rest : rest.slice(0, slash);
299
+ }
300
+ function isOwnedReference(url, knownPackages) {
301
+ if (url.startsWith("package://")) return true;
302
+ const match = /^\/([^/]+)\//.exec(url);
303
+ return match !== null && knownPackages.has(match[1]);
304
+ }
305
+ function isNetworkFetchableAbsoluteUrl(url) {
306
+ return /^https?:\/\//i.test(url);
307
+ }
308
+ function isRenderKind(kind) {
309
+ switch (kind) {
310
+ case "mesh":
311
+ case "texture":
312
+ return true;
313
+ case "urdf":
314
+ case "other":
315
+ return false;
316
+ default: {
317
+ const exhaustive = kind;
318
+ return exhaustive;
319
+ }
320
+ }
321
+ }
322
+ async function forEachWithConcurrency(items, limit, fn) {
323
+ let next = 0;
324
+ let failed = false;
325
+ let firstError;
326
+ async function worker() {
327
+ for (; ; ) {
328
+ const index = next++;
329
+ if (index >= items.length) return;
330
+ try {
331
+ await fn(items[index]);
332
+ } catch (error) {
333
+ if (!failed) {
334
+ failed = true;
335
+ firstError = error;
336
+ }
337
+ }
338
+ }
339
+ }
340
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
341
+ if (failed) throw firstError;
342
+ }
343
+ function createAssetsApi(http) {
344
+ async function list(robotId, signal) {
345
+ const response = await http.request(`/api/robots/${pathSegment(robotId)}/assets`, { signal });
346
+ return response;
347
+ }
348
+ async function get(robotId, assetId, signal) {
349
+ const { body, headers } = await http.requestBinary(`/api/robots/${pathSegment(robotId)}/assets/${pathSegment(assetId)}`, { signal });
350
+ return { body, mime: mimeFromContentType(headers) };
351
+ }
352
+ return {
353
+ list,
354
+ get,
355
+ async urdf(robotId) {
356
+ const { body } = await http.requestBinary(`/api/robots/${pathSegment(robotId)}/urdf`);
357
+ return new TextDecoder().decode(body);
358
+ },
359
+ createMeshLoader(robotId, delegate, options = {}) {
360
+ const timeoutMs = options.timeoutMs ?? DEFAULT_MESH_LOADER_TIMEOUT_MS;
361
+ return (path, manager, material, onComplete) => {
362
+ let settled = false;
363
+ let objectUrl = null;
364
+ const finish = (obj, err) => {
365
+ if (settled) return;
366
+ settled = true;
367
+ if (objectUrl) URL.revokeObjectURL(objectUrl);
368
+ onComplete(obj, err);
369
+ };
370
+ if (typeof manager === "object" && manager !== null && managersWithPreparedUrdfScene.has(manager)) {
371
+ finish(
372
+ null,
373
+ new Error(
374
+ `createMeshLoader and prepareUrdfScene were both installed on the same LoadingManager for robot ${robotId}. Use one or the other on a given manager, not both \u2014 see the README.`
375
+ )
376
+ );
377
+ return;
378
+ }
379
+ const timer = setTimeout(() => {
380
+ finish(null, new Error(`Mesh '${path}' on robot ${robotId} did not finish loading within ${timeoutMs}ms.`));
381
+ }, timeoutMs);
382
+ http.requestBinary(path).then(
383
+ ({ body, headers }) => {
384
+ if (settled) return;
385
+ const mime = mimeFromContentType(headers) ?? "application/octet-stream";
386
+ objectUrl = URL.createObjectURL(new Blob([body], { type: mime }));
387
+ try {
388
+ delegate(objectUrl, manager, material, (obj, err) => {
389
+ clearTimeout(timer);
390
+ finish(obj, err);
391
+ });
392
+ } catch (delegateError) {
393
+ clearTimeout(timer);
394
+ finish(null, delegateError instanceof Error ? delegateError : new Error(String(delegateError)));
395
+ }
396
+ },
397
+ (fetchError) => {
398
+ clearTimeout(timer);
399
+ finish(null, fetchError instanceof Error ? fetchError : new Error(String(fetchError)));
400
+ }
401
+ );
402
+ };
403
+ },
404
+ async prepareUrdfScene(robotId, manager, options = {}) {
405
+ const concurrency = options.concurrency ?? DEFAULT_SCENE_LOAD_CONCURRENCY;
406
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
407
+ throw new FleetlessError(
408
+ "invalid_option",
409
+ `prepareUrdfScene: options.concurrency must be a positive integer, got ${concurrency}.`
410
+ );
411
+ }
412
+ const signal = options.signal;
413
+ const aborted = () => new FleetlessError("aborted", `prepareUrdfScene for robot ${robotId} was aborted.`);
414
+ if (signal?.aborted) throw aborted();
415
+ const listResponse = await list(robotId, signal).catch((error) => {
416
+ throw signal?.aborted ? aborted() : error;
417
+ });
418
+ const urdfAsset = listResponse.assets.find((asset2) => asset2.kind === "urdf");
419
+ if (!urdfAsset) {
420
+ throw new FleetlessError(
421
+ "no_urdf_synced",
422
+ `Robot ${robotId} has no synced URDF \u2014 assets.list() has no 'urdf'-kind row. Sync one first (console, Owner-tier).`
423
+ );
424
+ }
425
+ const { body: urdfBytes } = await get(robotId, urdfAsset.id, signal).catch((error) => {
426
+ throw signal?.aborted ? aborted() : error;
427
+ });
428
+ const urdfText = new TextDecoder().decode(urdfBytes);
429
+ const renderAssets = listResponse.assets.filter((asset2) => isRenderKind(asset2.kind));
430
+ const knownPackages = /* @__PURE__ */ new Set();
431
+ for (const asset2 of renderAssets) {
432
+ const pkg = packageNameOf(asset2.name);
433
+ if (pkg) knownPackages.add(pkg);
434
+ }
435
+ for (const ref of listResponse.urdf.missing) {
436
+ const pkg = packageNameOf(ref);
437
+ if (pkg) knownPackages.add(pkg);
438
+ }
439
+ const refusedObjectUrl = URL.createObjectURL(new Blob([]));
440
+ const objectUrls = [];
441
+ const nameToObjectUrl = /* @__PURE__ */ new Map();
442
+ try {
443
+ await forEachWithConcurrency(renderAssets, concurrency, async (asset2) => {
444
+ const { body, mime } = await get(robotId, asset2.id, signal);
445
+ const objectUrl = URL.createObjectURL(new Blob([body], { type: mime ?? "application/octet-stream" }));
446
+ objectUrls.push(objectUrl);
447
+ for (const key of urlModifierKeysFor(asset2.name)) nameToObjectUrl.set(key, objectUrl);
448
+ });
449
+ } catch (error) {
450
+ for (const objectUrl of objectUrls) URL.revokeObjectURL(objectUrl);
451
+ throw signal?.aborted ? aborted() : error;
452
+ }
453
+ if (signal?.aborted) {
454
+ for (const objectUrl of objectUrls) URL.revokeObjectURL(objectUrl);
455
+ throw aborted();
456
+ }
457
+ if (typeof manager === "object" && manager !== null) managersWithPreparedUrdfScene.add(manager);
458
+ manager.setURLModifier((url) => {
459
+ const direct = nameToObjectUrl.get(url);
460
+ if (direct) return direct;
461
+ if (!isOwnedReference(url, knownPackages)) {
462
+ return isNetworkFetchableAbsoluteUrl(url) ? refusedObjectUrl : url;
463
+ }
464
+ return nameToObjectUrl.get(normalizedUrlModifierKey(url)) ?? refusedObjectUrl;
465
+ });
466
+ let disposed = false;
467
+ return {
468
+ urdfText,
469
+ missing: listResponse.urdf.missing,
470
+ dispose() {
471
+ if (disposed) return;
472
+ disposed = true;
473
+ for (const objectUrl of objectUrls) URL.revokeObjectURL(objectUrl);
474
+ nameToObjectUrl.clear();
475
+ }
476
+ };
477
+ }
478
+ };
479
+ }
480
+
481
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/common.js
482
+ var import_zod = require("zod");
483
+ var slug = import_zod.z.string().min(2).max(63).regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/);
484
+ var rosName = import_zod.z.string().max(255).regex(/^\/[A-Za-z_][A-Za-z0-9_]*(?:\/[A-Za-z_][A-Za-z0-9_]*)*$/);
485
+ var rosTypeName = import_zod.z.string().max(255).regex(/^[a-z][a-z0-9_]*\/(?:msg|srv|action)\/[A-Za-z][A-Za-z0-9]*$/);
486
+ var fieldPath = import_zod.z.string().max(255).regex(/^[a-z_][a-z0-9_]*(?:\[\d+\])*(?:\.[a-z_][a-z0-9_]*(?:\[\d+\])*)*$/);
487
+
488
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/mcp.js
489
+ var import_zod2 = require("zod");
490
+ var mcpToolKind = import_zod2.z.enum(["datapoint", "service", "action", "publisher", "camera"]);
491
+ var MCP_TOOL_NAME_MAX = 128;
492
+ var mcpToolNamePattern = /^[a-z0-9][a-z0-9_-]*$/;
493
+ var mcpToolPreview = import_zod2.z.object({
494
+ name: import_zod2.z.string().min(1).max(MCP_TOOL_NAME_MAX).regex(mcpToolNamePattern),
495
+ /** The human-readable label — where the robot's actual name goes. */
496
+ title: import_zod2.z.string().min(1).max(200),
497
+ /**
498
+ * **Four thousand, not two thousand, and the difference is the point.**
499
+ *
500
+ * `serviceDescription` bounds what a *developer writes* at 2000. This bounds
501
+ * what the *generator produces*, which is that text **plus** what it folds
502
+ * in — a datapoint's `Unit:` and `Plausible range:`, a camera's fixed
503
+ * sentence about snapshots. Measured by Kassandra-W7c and Momus-W7c
504
+ * independently: a maximal description came back at 2036–2068 characters
505
+ * against a 2000 bound, so the cloud served a document its own contract
506
+ * rejected — silently, because the route returns a typed literal without
507
+ * parsing it.
508
+ *
509
+ * **Do not "tidy" these two numbers into agreement.** They describe
510
+ * different things, and making them equal reintroduces the defect: either
511
+ * the generator truncates a developer's own words, or the response
512
+ * overflows again. The gap is the room the generator needs.
513
+ */
514
+ description: import_zod2.z.string().min(1).max(4e3),
515
+ robot_id: import_zod2.z.uuid(),
516
+ slug,
517
+ kind: mcpToolKind,
518
+ input_schema: import_zod2.z.unknown()
519
+ });
520
+ var mcpOmission = import_zod2.z.object({
521
+ robot_id: import_zod2.z.uuid(),
522
+ slug,
523
+ /** One of `MCP_OMISSION_REASONS`; the wire allows any string, as with `ERROR_CODES`. */
524
+ reason: import_zod2.z.string().min(1),
525
+ message: import_zod2.z.string().min(1)
526
+ });
527
+ var mcpToolPreviewResponse = import_zod2.z.object({
528
+ role_id: import_zod2.z.uuid(),
529
+ tools: import_zod2.z.array(mcpToolPreview).max(500),
530
+ omitted: import_zod2.z.array(mcpOmission).max(500)
531
+ });
532
+
533
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/protocol.js
534
+ var import_zod7 = require("zod");
535
+
536
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/assets.js
537
+ var import_zod3 = require("zod");
538
+ var assetKind = import_zod3.z.enum(["urdf", "mesh", "texture", "other"]);
539
+ var asset = import_zod3.z.object({
540
+ id: import_zod3.z.uuid(),
541
+ robot_id: import_zod3.z.uuid(),
542
+ kind: assetKind,
543
+ /**
544
+ * What the robot called it — for a mesh, the `package://` URI the URDF
545
+ * references, verbatim. That is the only string a developer can match
546
+ * against their own workspace, and matching is the whole job when a sync
547
+ * comes back incomplete.
548
+ *
549
+ * **The naming rule for a file nothing in the URDF names (W7a, D2).** A
550
+ * `.dae` carries its own image references — `<init_from>textures/skin.png`
551
+ * — resolved by the renderer against *the `.dae`'s own directory*, and no
552
+ * `package://` URI for them appears anywhere in the URDF. The rule is:
553
+ *
554
+ * name = the .dae's package:// URI, directory part,
555
+ * joined with the internal reference, normalized.
556
+ *
557
+ * So `package://rx1_description/meshes/arm.dae` referencing
558
+ * `textures/skin.png` uploads as
559
+ * `package://rx1_description/meshes/textures/skin.png`.
560
+ *
561
+ * **This is the design's single point of failure and it is stated before
562
+ * anything is built against it.** three.js resolves that internal reference
563
+ * relative to wherever it loaded the `.dae` from and asks the loading
564
+ * manager for the result; the client can only answer if the asset's name
565
+ * still carries the same **relative tail** (`textures/skin.png`) that the
566
+ * `.dae` asked for. Normalizing into a `package://` URI preserves that tail
567
+ * exactly, keeps every name in one namespace a developer already reads, and
568
+ * keeps `urdfCompleteness.missing` meaningful for files the URDF never
569
+ * mentioned.
570
+ *
571
+ * A reference that escapes its package (`../../etc/passwd`) is **not**
572
+ * renamed into something harmless — it is refused at the producer, by the
573
+ * same containment check W7's K2 fix applied to `package://` resolution.
574
+ * Two identical rules, one of which is enforced and one of which is
575
+ * documented, is how W7's traversal happened in the first place.
576
+ */
577
+ name: import_zod3.z.string().min(1).max(500),
578
+ media_type: import_zod3.z.string().min(1).max(120),
579
+ size_bytes: import_zod3.z.number().int().nonnegative(),
580
+ /**
581
+ * The content hash, and the reason two robots sharing a mesh cost one copy.
582
+ *
583
+ * Exposed rather than kept internal because it is the only way a client can
584
+ * tell "this is the same mesh I already have" across robots — and a 3D view
585
+ * that re-downloads an identical arm for every robot in a fleet is the
586
+ * predictable failure of a store that hides it.
587
+ */
588
+ sha256: import_zod3.z.string().regex(/^[a-f0-9]{64}$/),
589
+ created_at: import_zod3.z.iso.datetime()
590
+ });
591
+ var urdfCompleteness = import_zod3.z.object({
592
+ /** Whether a URDF has been synced at all. Availability is a different question. */
593
+ present: import_zod3.z.boolean(),
594
+ /** How many distinct meshes the URDF references. */
595
+ mesh_count: import_zod3.z.number().int().nonnegative(),
596
+ missing: import_zod3.z.array(import_zod3.z.string().min(1))
597
+ });
598
+ var assetListResponse = import_zod3.z.object({
599
+ assets: import_zod3.z.array(asset),
600
+ urdf: urdfCompleteness,
601
+ /**
602
+ * What the connected bridge says it *could* transfer, which is deliberately
603
+ * separate from what has been transferred (§4.6: the bridge "meldet nur
604
+ * Verfügbarkeit"). `null` when no bridge is connected — distinct from
605
+ * `false`, because "no robot is online to ask" and "the robot has no URDF"
606
+ * send a developer to two different places.
607
+ *
608
+ * **All three states are reachable as of W7a (R7).** They were not: the bridge
609
+ * used to report availability from a subscription callback, which fires only
610
+ * when a publisher *sends* something, so it could notice presence and never
611
+ * absence — a robot that lost its URDF left the cloud holding the last thing
612
+ * it heard, forever, and `true` was sticky. The fix is an **active**
613
+ * `count_publishers` query on the bridge's own timer.
614
+ *
615
+ * **What a consumer still needs to know is the clock, not the gap.** An
616
+ * ungraceful loss — the publisher process killed rather than shut down — is
617
+ * noticed on **DDS's liveliness timeout**, not on the bridge's check
618
+ * interval. Measured against a real bridge: ~1.6 s when the publisher calls
619
+ * `destroy_node()`, **~19 s when it is `SIGKILL`ed**. So `true` can outlive
620
+ * the truth by some seconds after a crash, and no amount of polling on our
621
+ * side shortens it.
622
+ *
623
+ * The sticky-`true` gap was found by Rosie-W7 checking her own work against
624
+ * the camera-health row of identical shape; the DDS clock was measured by
625
+ * Rosie-W7a closing it, and this comment was still describing the gap a wave
626
+ * after it was fixed (Momus-W7a, W7a review).
627
+ */
628
+ urdf_available: import_zod3.z.boolean().nullable()
629
+ });
630
+ var assetSyncRequest = import_zod3.z.object({
631
+ source: import_zod3.z.enum(["bridge"])
632
+ }).strict();
633
+ var assetSyncResponse = import_zod3.z.object({
634
+ sync_id: import_zod3.z.uuid()
635
+ });
636
+ var assetFailureKind = import_zod3.z.enum(["unresolvable", "upload_failed", "refused"]);
637
+ var assetFailure = import_zod3.z.object({
638
+ /**
639
+ * What could not be provided, verbatim — the same string `asset.name` would
640
+ * have stored and `urdfCompleteness.missing` reports, so a developer can
641
+ * match it against their own workspace by eye. For a URDF upload failure it
642
+ * is `URDF_ASSET_NAME`, which is **not** a mesh URI: a consumer rendering
643
+ * this list must not assume every entry is one.
644
+ */
645
+ reference: import_zod3.z.string().min(1).max(500),
646
+ kind: assetFailureKind
647
+ });
648
+ var assetSyncState = import_zod3.z.enum(["running", "succeeded", "failed"]);
649
+ var assetSyncStatus = import_zod3.z.object({
650
+ sync_id: import_zod3.z.uuid(),
651
+ robot_id: import_zod3.z.uuid(),
652
+ state: assetSyncState,
653
+ done: import_zod3.z.number().int().nonnegative(),
654
+ total: import_zod3.z.number().int().nonnegative(),
655
+ /**
656
+ * **Every entry says *why*, because reconciliation could not work without
657
+ * it and a developer could not read it without it** (W7a review, André's
658
+ * decision to fix rather than defer).
659
+ *
660
+ * It was a flat `string[]`, and **six producers wrote three different facts
661
+ * into it indistinguishably**: a reference that resolves to nothing in the
662
+ * workspace, a file that exists and whose transfer failed, and — since R9's
663
+ * ceiling — one that was never attempted at all. The cost was paid twice
664
+ * over. Reconciliation cannot tell *"no longer referenced"* from
665
+ * *"referenced and not delivered"*, so N14 had to decline reconciling **any**
666
+ * partial sync, leaving legitimately-removed assets stored and charged until
667
+ * the next clean one. And the console prints the whole array under *"these
668
+ * meshes could not be resolved"*, so a URDF upload failure — which arrives
669
+ * as the literal `robot_description` — is shown to a developer as a mesh
670
+ * they should go and find.
671
+ *
672
+ * `unresolvable` is the only kind reconciliation may drop: it is the only
673
+ * one that means *this will not come back*. `upload_failed` and `refused`
674
+ * both mean *we meant to provide this and did not*, which is the distinction
675
+ * the union needs and the field could not carry.
676
+ *
677
+ * **Bounded, and the bound is a rule this file already wrote down one field
678
+ * over** (Kassandra-W7a, W7a review). `asset.name` is `max(500)`; the same
679
+ * names travelling here had no per-entry cap and no array cap at all.
680
+ *
681
+ * Why it matters became reachable in W7a. Before R6 an unresolvable
682
+ * reference was silently dropped, so this array could only grow with files
683
+ * that existed and failed to upload — bounded by the workspace. Once a
684
+ * `.dae`'s internal references are reported, **one mesh reference expands
685
+ * into a list bounded only by that file's own text.** Measured: a
686
+ * 2,120,745-byte `.dae` with 17,331 unresolvable `<init_from>` refs produces
687
+ * a terminal frame of 2,097,184 bytes — **32 bytes over
688
+ * `MAX_WS_PAYLOAD_BYTES`** — and `ws` enforces `maxPayload` before the frame
689
+ * is delivered, so the outcome is not a dropped frame but **the robot's
690
+ * socket closed, mid-sync, by a file in its own workspace.**
691
+ *
692
+ * A producer that hits its own ceiling reports **one** entry saying so
693
+ * rather than growing the list — the discipline gate step 5 already demands
694
+ * of the upload rate limit: *fail naming the limit, rather than silently
695
+ * reporting resolvable meshes as missing.*
696
+ *
697
+ * 1000 x 500 bytes is ~0.5 MiB of names, comfortably inside a 2 MiB frame.
698
+ */
699
+ failed: import_zod3.z.array(assetFailure).max(1e3),
700
+ /** Why the sync ended as it did, when that is not a per-URI fact. */
701
+ reason: import_zod3.z.string().min(1).nullable(),
702
+ started_at: import_zod3.z.iso.datetime(),
703
+ updated_at: import_zod3.z.iso.datetime()
704
+ });
705
+ var assetTooLargeDetails = import_zod3.z.object({
706
+ limit_bytes: import_zod3.z.number().int().positive(),
707
+ size_bytes: import_zod3.z.number().int().positive()
708
+ });
709
+
710
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/config.js
711
+ var import_zod4 = require("zod");
712
+ var valueRule = import_zod4.z.object({
713
+ min: import_zod4.z.number().optional(),
714
+ max: import_zod4.z.number().optional(),
715
+ enum: import_zod4.z.array(import_zod4.z.union([import_zod4.z.string(), import_zod4.z.number()])).min(1).optional(),
716
+ pattern: import_zod4.z.string().optional(),
717
+ required: import_zod4.z.boolean().optional()
718
+ });
719
+ var serviceDescription = import_zod4.z.string().min(1).max(2e3).optional();
720
+ var parameterDescription = import_zod4.z.string().min(1).max(500).optional();
721
+ var parameterSpec = import_zod4.z.object({
722
+ /** Field path into the ROS request/goal/message — same grammar as a datapoint's. */
723
+ name: fieldPath,
724
+ /** The ROS type, for the console to render an input the developer recognises. */
725
+ type: import_zod4.z.string().min(1).max(255),
726
+ rule: valueRule,
727
+ description: parameterDescription
728
+ });
729
+ var datapointRate = import_zod4.z.discriminatedUnion("mode", [
730
+ import_zod4.z.object({ mode: import_zod4.z.literal("max_hz"), hz: import_zod4.z.number().positive().max(100) }),
731
+ import_zod4.z.object({ mode: import_zod4.z.literal("on_change") })
732
+ ]);
733
+ var datapointRange = import_zod4.z.object({
734
+ min: import_zod4.z.number().nullable(),
735
+ max: import_zod4.z.number().nullable()
736
+ });
737
+ var datapointConfig = import_zod4.z.object({
738
+ slug,
739
+ topic: rosName,
740
+ type: rosTypeName,
741
+ field: fieldPath.nullable(),
742
+ rate: datapointRate,
743
+ unit: import_zod4.z.string().max(32).nullable(),
744
+ scale: import_zod4.z.number().nullable(),
745
+ offset: import_zod4.z.number().nullable(),
746
+ range: datapointRange.nullable(),
747
+ description: serviceDescription,
748
+ /**
749
+ * Record this datapoint (spec §8). Recorded values go to the time-series
750
+ * store and are queryable through the history API; everything else is
751
+ * live-only and leaves no trace.
752
+ *
753
+ * **Exactly one representation of "not recorded": `false`.** W5 reserved
754
+ * this field as `z.null().optional()`, so stored documents may carry
755
+ * `retention: null` — the cloud normalises that to `false` on read rather
756
+ * than the contract accepting both, because two spellings of one fact is
757
+ * the defect this project has spent two waves removing.
758
+ *
759
+ * Defaulted so a document written before W6 still parses. Note that
760
+ * `.default()` publishes as `required` in the generated artifact — the
761
+ * fourth instance, deferred to W7 with the fix identified
762
+ * (`io: 'input'`, split per schema).
763
+ */
764
+ retention: import_zod4.z.boolean().default(false),
765
+ /**
766
+ * What happens to this datapoint's values while the bridge is disconnected
767
+ * (spec §6.3). Buffered values are backfilled after reconnect — **after**
768
+ * live telemetry and job results, at a limited rate, so closing a gap can
769
+ * never delay what is happening now. An unbuffered datapoint simply has a
770
+ * gap, which is an honest answer and often the right one.
771
+ */
772
+ buffer: import_zod4.z.object({
773
+ enabled: import_zod4.z.boolean(),
774
+ /**
775
+ * Zero is legal and means "no depth" — it is what a disabled buffer
776
+ * carries. The invariant that matters is stated below: *enabled*
777
+ * implies a depth greater than zero.
778
+ */
779
+ max_values: import_zod4.z.number().int().nonnegative().max(1e5)
780
+ }).refine((b) => !b.enabled || b.max_values > 0, {
781
+ message: "an enabled buffer needs max_values > 0",
782
+ path: ["max_values"]
783
+ }).default({ enabled: false, max_values: 0 })
784
+ });
785
+ var actionConfig = import_zod4.z.object({
786
+ slug,
787
+ ros_name: rosName,
788
+ type: rosTypeName,
789
+ parameters: import_zod4.z.array(parameterSpec).max(50),
790
+ description: serviceDescription
791
+ });
792
+ var serviceConfig = import_zod4.z.object({
793
+ slug,
794
+ ros_name: rosName,
795
+ type: rosTypeName,
796
+ parameters: import_zod4.z.array(parameterSpec).max(50),
797
+ description: serviceDescription
798
+ });
799
+ var publisherConfig = import_zod4.z.object({
800
+ slug,
801
+ topic: rosName,
802
+ type: rosTypeName,
803
+ parameters: import_zod4.z.array(parameterSpec).max(50),
804
+ timeout_ms: import_zod4.z.number().int().positive().max(6e4),
805
+ /** The message the bridge publishes on timeout. Shape is the ROS type's. */
806
+ failsafe: import_zod4.z.unknown(),
807
+ quiet_timeout_ms: import_zod4.z.number().int().nonnegative().max(6e5),
808
+ description: serviceDescription
809
+ });
810
+ var credentialRef = import_zod4.z.string().min(1).max(64);
811
+ var cameraSource = import_zod4.z.discriminatedUnion("kind", [
812
+ import_zod4.z.object({
813
+ kind: import_zod4.z.literal("ros"),
814
+ topic: rosName,
815
+ /** `sensor_msgs/msg/Image` or `sensor_msgs/msg/CompressedImage`. */
816
+ type: rosTypeName
817
+ }),
818
+ import_zod4.z.object({
819
+ kind: import_zod4.z.literal("rtsp"),
820
+ /**
821
+ * Scheme-constrained deliberately. The playbook drafted `z.string().url()`
822
+ * here and the shipped contract was `z.string().min(1).max(2048)` — nobody
823
+ * recorded the change, and the W6 review found the consequence: the bridge
824
+ * opens these with libraries that honour `file:` and `ftp:`, so an
825
+ * unconstrained URL turns a configuration document into an arbitrary
826
+ * local-file read on the robot, with the two distinct failure codes
827
+ * doubling as a file-existence oracle. Spec §7.6 is ROS-pure exposure with
828
+ * no shell or http features; that rule came back by omission rather than
829
+ * by intent. The bridge re-checks this too — a robot must not become a
830
+ * file server because a validator changed.
831
+ */
832
+ url: import_zod4.z.string().min(1).max(2048).regex(/^rtsps?:\/\//i, "must be an rtsp:// or rtsps:// URL"),
833
+ /** TCP by default: UDP loses frames on a congested link, silently. */
834
+ transport: import_zod4.z.enum(["tcp", "udp"]).default("tcp"),
835
+ credentials_ref: credentialRef.nullable().default(null)
836
+ }),
837
+ import_zod4.z.object({
838
+ kind: import_zod4.z.literal("mjpeg"),
839
+ /** `http:`/`https:` only — see the `rtsp` variant above for why. */
840
+ url: import_zod4.z.string().min(1).max(2048).regex(/^https?:\/\//i, "must be an http:// or https:// URL"),
841
+ credentials_ref: credentialRef.nullable().default(null)
842
+ }),
843
+ import_zod4.z.object({
844
+ kind: import_zod4.z.literal("v4l2"),
845
+ /**
846
+ * e.g. `/dev/video0`, or a stable `/dev/v4l/by-id/...` symlink. Resolved
847
+ * on the robot, never by the cloud.
848
+ *
849
+ * Constrained to `/dev/` for the same reason the `rtsp` and `mjpeg` URLs
850
+ * are constrained to their schemes, and it was missed the first time
851
+ * (Momus, W6 verification). The device string reaches
852
+ * `cv2.VideoCapture(device)` on the robot, and OpenCV does not restrict
853
+ * itself to devices: measured on cv2 4.5.4, an ordinary local video file
854
+ * opens and its pixels are published to the cloud, and so does
855
+ * `http://127.0.0.1:8899/secret.jpg`. Unconstrained, this field is an
856
+ * arbitrary local-file read *and* an outbound fetch from inside the robot
857
+ * — the §7.6 violation closed for the other two source kinds, reachable
858
+ * through the fourth, because "it is just a device path" read like a
859
+ * reason not to check.
860
+ *
861
+ * Narrower than the URL hole in one respect worth recording: a non-media
862
+ * file and a missing file both fail to open, so this branch never worked
863
+ * as a file-existence oracle.
864
+ *
865
+ * The bridge re-derives this constraint rather than trusting the wire
866
+ * (`validate_device_path`), exactly as it re-derives the URL scheme.
867
+ */
868
+ device: import_zod4.z.string().min(1).max(128).regex(/^\/dev\/[A-Za-z0-9][A-Za-z0-9._/-]*$/, "must be a device path under /dev/").refine((v) => !v.split("/").includes(".."), "must not contain a `..` path segment").refine((v) => !v.endsWith("/"), "must name a device, not a directory")
869
+ })
870
+ ]);
871
+ var cameraConfig = import_zod4.z.object({
872
+ slug,
873
+ source: cameraSource,
874
+ width: import_zod4.z.number().int().positive().max(7680),
875
+ height: import_zod4.z.number().int().positive().max(4320),
876
+ fps: import_zod4.z.number().int().positive().max(60),
877
+ bitrate_kbps: import_zod4.z.number().int().positive().max(5e4),
878
+ /**
879
+ * How often a snapshot is captured. Bounded below at one second because a
880
+ * snapshot is the *cheap* mode — a developer who wants motion wants live,
881
+ * and an interval faster than this is a live stream wearing a disguise.
882
+ */
883
+ snapshot_interval_ms: import_zod4.z.number().int().min(1e3).max(36e5),
884
+ description: serviceDescription
885
+ });
886
+ var robotConfigDoc = import_zod4.z.object({
887
+ datapoints: import_zod4.z.array(datapointConfig).max(200),
888
+ /**
889
+ * The three kinds W4 adds default to empty so that **every configuration
890
+ * published before W4 still parses**. Stored documents are jsonb; a
891
+ * required field here would have invalidated live robots' published
892
+ * versions on the first read after deploy.
893
+ */
894
+ actions: import_zod4.z.array(actionConfig).max(200).default([]),
895
+ services: import_zod4.z.array(serviceConfig).max(200).default([]),
896
+ publishers: import_zod4.z.array(publisherConfig).max(200).default([]),
897
+ /** W5, defaulted for the same reason the W4 kinds were: stored jsonb. */
898
+ cameras: import_zod4.z.array(cameraConfig).max(50).default([])
899
+ });
900
+ var validationIssue = import_zod4.z.object({
901
+ path: import_zod4.z.string().min(1),
902
+ slug: import_zod4.z.string().nullable(),
903
+ code: import_zod4.z.string().min(1),
904
+ message: import_zod4.z.string().min(1),
905
+ severity: import_zod4.z.enum(["error", "warning"])
906
+ });
907
+ var configState = import_zod4.z.object({
908
+ published_version: import_zod4.z.number().int().positive().nullable(),
909
+ published_at: import_zod4.z.iso.datetime().nullable(),
910
+ draft_updated_at: import_zod4.z.iso.datetime().nullable(),
911
+ applied_version: import_zod4.z.number().int().nonnegative().nullable(),
912
+ applied_ok: import_zod4.z.boolean().nullable(),
913
+ applied_errors: import_zod4.z.array(import_zod4.z.object({ slug: import_zod4.z.string(), message: import_zod4.z.string() })).nullable()
914
+ });
915
+
916
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/introspection.js
917
+ var import_zod5 = require("zod");
918
+ var rosGraphEntry = import_zod5.z.object({
919
+ name: rosName,
920
+ types: import_zod5.z.array(rosTypeName).min(1)
921
+ });
922
+ var rosGraph = import_zod5.z.object({
923
+ topics: import_zod5.z.array(rosGraphEntry),
924
+ services: import_zod5.z.array(rosGraphEntry),
925
+ actions: import_zod5.z.array(rosGraphEntry),
926
+ captured_at_ms: import_zod5.z.number().int().nonnegative()
927
+ });
928
+ var typeField = import_zod5.z.lazy(() => import_zod5.z.object({
929
+ name: import_zod5.z.string().min(1).max(128),
930
+ type: import_zod5.z.string().min(1).max(255),
931
+ array: import_zod5.z.boolean(),
932
+ fields: import_zod5.z.array(typeField).nullable()
933
+ }));
934
+ var typeDefinition = import_zod5.z.discriminatedUnion("kind", [
935
+ import_zod5.z.object({
936
+ name: rosTypeName,
937
+ kind: import_zod5.z.literal("msg"),
938
+ fields: import_zod5.z.array(typeField)
939
+ }),
940
+ import_zod5.z.object({
941
+ name: rosTypeName,
942
+ kind: import_zod5.z.literal("srv"),
943
+ request: import_zod5.z.array(typeField),
944
+ response: import_zod5.z.array(typeField)
945
+ }),
946
+ import_zod5.z.object({
947
+ name: rosTypeName,
948
+ kind: import_zod5.z.literal("action"),
949
+ goal: import_zod5.z.array(typeField),
950
+ result: import_zod5.z.array(typeField),
951
+ feedback: import_zod5.z.array(typeField)
952
+ })
953
+ ]);
954
+
955
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/jobs.js
956
+ var import_zod6 = require("zod");
957
+ var jobState = import_zod6.z.enum(["running", "succeeded", "failed", "cancelled", "lost"]);
958
+ var job = import_zod6.z.object({
959
+ id: import_zod6.z.uuid(),
960
+ robot_id: import_zod6.z.uuid(),
961
+ slug,
962
+ state: jobState,
963
+ started_at: import_zod6.z.iso.datetime(),
964
+ updated_at: import_zod6.z.iso.datetime(),
965
+ /**
966
+ * A monotonic counter, ascending in mint order (W7), and the **named**
967
+ * tiebreaker for any listing that claims an order.
968
+ *
969
+ * `started_at` is not a total order: two jobs minted in the same millisecond
970
+ * sort against each other arbitrarily, and arbitrarily means *differently on
971
+ * each query* — so `GET /api/robots/:id/jobs`, which documents "newest
972
+ * first", can show one twice and the other not at all. Exactly the defect
973
+ * `auditEvent.seq` was added for in W6b, in a route the same wave shipped.
974
+ *
975
+ * **Scoped honestly: per cloud process, per run.** Job state lives in memory
976
+ * (§6.1 — that is why `lost` exists at all), so this counter restarts when
977
+ * the cloud does, alongside the jobs it orders. Sound, because it only ever
978
+ * orders jobs that coexist in one registry — and stated, because a reader
979
+ * who assumed `auditEvent.seq`'s durable semantics would be wrong.
980
+ */
981
+ seq: import_zod6.z.number().int().positive(),
982
+ /** Present once the job succeeded; shape is the ROS result's. */
983
+ result: import_zod6.z.unknown().nullable(),
984
+ /**
985
+ * Present on `failed`; a human message, plus a code where one exists.
986
+ *
987
+ * `details` exists because a refusal that carries only prose forces every
988
+ * consumer to parse it. W6b shipped `job_queue_full` with a documented
989
+ * `{limit, queued}` payload and **nowhere to put it**: the bridge reports a
990
+ * full queue as a job error, this shape had no `details`, and so the numbers
991
+ * were formatted into the message and lost. The console then rendered a
992
+ * "wait for one of N to finish" alert from a shape nothing in the system
993
+ * produced, and its test built that shape by hand — three repos agreeing
994
+ * with each other about a payload none of them exchanged (Momus, W6b
995
+ * review).
996
+ *
997
+ * Optional, because most job errors have nothing structured to add. Where a
998
+ * code has a documented payload — `job_queue_full` has
999
+ * `jobQueueFullDetails` — it belongs here, not in the sentence.
1000
+ */
1001
+ error: import_zod6.z.object({
1002
+ code: import_zod6.z.string().min(1),
1003
+ message: import_zod6.z.string().min(1),
1004
+ details: import_zod6.z.unknown().optional()
1005
+ }).nullable()
1006
+ });
1007
+ var jobEvent = import_zod6.z.object({
1008
+ type: import_zod6.z.literal("job"),
1009
+ robot_id: import_zod6.z.uuid(),
1010
+ slug,
1011
+ job,
1012
+ /** Action feedback, if this update carries any. */
1013
+ feedback: import_zod6.z.unknown().nullable(),
1014
+ /** 0..1 when the action reports progress; null when it does not. */
1015
+ progress: import_zod6.z.number().min(0).max(1).nullable(),
1016
+ timestamp_ms: import_zod6.z.number().int().nonnegative()
1017
+ });
1018
+ var busyDetails = import_zod6.z.object({
1019
+ running: job
1020
+ });
1021
+ var publisherBusyDetails = import_zod6.z.object({
1022
+ /** The configured silence a holder must leave before anyone else may publish. */
1023
+ quiet_timeout_ms: import_zod6.z.number().int().nonnegative(),
1024
+ /** How much of that silence is still outstanding, now. */
1025
+ retry_after_ms: import_zod6.z.number().int().nonnegative()
1026
+ });
1027
+ var jobQueueFullDetails = import_zod6.z.object({
1028
+ /** The bridge's bound on queued jobs. */
1029
+ limit: import_zod6.z.number().int().positive(),
1030
+ /** How many are queued right now — `>= limit` when this refusal is sent. */
1031
+ queued: import_zod6.z.number().int().nonnegative()
1032
+ });
1033
+
1034
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/protocol.js
1035
+ var MAX_PATIENCE_MS = 12e4;
1036
+ var MIN_PATIENCE_MS = 1e3;
1037
+ var activeJob = import_zod7.z.object({
1038
+ job_id: import_zod7.z.uuid(),
1039
+ slug,
1040
+ state: jobState
1041
+ });
1042
+ var bridgeHello = import_zod7.z.object({
1043
+ type: import_zod7.z.literal("hello"),
1044
+ protocol_version: import_zod7.z.number().int().positive(),
1045
+ token: import_zod7.z.string().min(1),
1046
+ bridge_version: import_zod7.z.string().min(1),
1047
+ /**
1048
+ * Every job this bridge still knows about, right now (spec §6.1, W4).
1049
+ *
1050
+ * A reconnect and a restart look **identical** on the wire otherwise: same
1051
+ * token, same version, same frame. But they must end differently — after a
1052
+ * dropped connection the running jobs are still running, after a restart
1053
+ * their results are gone forever. Asking the bridge to enumerate what it
1054
+ * still has settles it without either side guessing: the cloud marks every
1055
+ * job it believes is running on this robot and that is *not* named here as
1056
+ * `lost`.
1057
+ *
1058
+ * This deliberately needs no persistence at the bridge. A live process
1059
+ * lists its live jobs; a process that just started lists none, because it
1060
+ * has none — which is exactly the truth the cloud needs. A breadcrumb file
1061
+ * would only add a window in which the crash beat the write.
1062
+ *
1063
+ * Defaulted so pre-W4 bridges still parse; they had no jobs, so the empty
1064
+ * list is also the correct answer for them.
1065
+ *
1066
+ * **Renamed from `active_job_ids` in W6b**, when the entries stopped being
1067
+ * ids. A field called `_ids` holding objects is the shape this project has
1068
+ * repeatedly been caught by — a name that describes what the field used to
1069
+ * carry, kept because renaming looked like churn. Nothing is deployed yet
1070
+ * (W8 is the first deployment), so the old name is gone rather than
1071
+ * accepted alongside the new one: two accepted spellings would have to be
1072
+ * supported and reconciled forever, and nobody is asking for that.
1073
+ */
1074
+ active_jobs: import_zod7.z.array(activeJob).max(500).default([])
1075
+ });
1076
+ var cloudHelloOk = import_zod7.z.object({
1077
+ type: import_zod7.z.literal("hello_ok"),
1078
+ robot_id: import_zod7.z.uuid()
1079
+ });
1080
+ var cloudHelloError = import_zod7.z.object({
1081
+ type: import_zod7.z.literal("hello_error"),
1082
+ code: import_zod7.z.string().min(1),
1083
+ message: import_zod7.z.string().min(1)
1084
+ });
1085
+ var datapointFrame = import_zod7.z.object({
1086
+ type: import_zod7.z.literal("datapoint"),
1087
+ slug,
1088
+ value: import_zod7.z.unknown(),
1089
+ timestamp_ms: import_zod7.z.number().int().nonnegative()
1090
+ });
1091
+ var cloudPing = import_zod7.z.object({
1092
+ type: import_zod7.z.literal("ping"),
1093
+ ts_ms: import_zod7.z.number().int().nonnegative()
1094
+ });
1095
+ var bridgePong = import_zod7.z.object({
1096
+ type: import_zod7.z.literal("pong"),
1097
+ ts_ms: import_zod7.z.number().int().nonnegative()
1098
+ });
1099
+ var cloudConfig = import_zod7.z.object({
1100
+ type: import_zod7.z.literal("config"),
1101
+ version: import_zod7.z.number().int().nonnegative(),
1102
+ doc: robotConfigDoc,
1103
+ credentials: import_zod7.z.record(credentialRef, import_zod7.z.object({ username: import_zod7.z.string(), password: import_zod7.z.string() })).default({})
1104
+ });
1105
+ var bridgeConfigApplied = import_zod7.z.object({
1106
+ type: import_zod7.z.literal("config_applied"),
1107
+ version: import_zod7.z.number().int().nonnegative(),
1108
+ ok: import_zod7.z.boolean(),
1109
+ errors: import_zod7.z.array(import_zod7.z.object({ slug: import_zod7.z.string(), message: import_zod7.z.string().min(1) }))
1110
+ });
1111
+ var cloudInvoke = import_zod7.z.object({
1112
+ type: import_zod7.z.literal("invoke"),
1113
+ job_id: import_zod7.z.uuid(),
1114
+ slug,
1115
+ /**
1116
+ * Already validated against §4.4 rules; the bridge validates structurally.
1117
+ *
1118
+ * **Flat, keyed by `parameterSpec.name`** — `{"target_pose.position.x": 1}`,
1119
+ * not a nested message tree. Three things follow from that and none of them
1120
+ * survive the nested form: the key a caller sends is the key a rule names,
1121
+ * so a `parameter_invalid` can report a `field` the caller can actually
1122
+ * find; the console binds one form input per spec; and a goal field that no
1123
+ * `parameterSpec` declares simply cannot be set, which is what §4.4 means by
1124
+ * the developer deciding what a client may pass. The bridge unflattens once,
1125
+ * on the way into the ROS goal or request.
1126
+ */
1127
+ params: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.unknown()),
1128
+ /**
1129
+ * How long this one call is worth waiting for (W6b), already resolved by
1130
+ * the cloud — the caller's `invokeRequest.patience_ms`, or
1131
+ * `DEFAULT_PATIENCE_MS` when they named none.
1132
+ *
1133
+ * **Required here, optional at REST**, deliberately. At the REST edge an
1134
+ * absent value is a caller who did not care and gets the default. By the
1135
+ * time the frame is on this socket somebody has decided, and the bridge
1136
+ * must never be in the position of picking a number the cloud is already
1137
+ * counting against — which is what two independent 15 s constants meant in
1138
+ * practice: a bridge that gave up at 15.0 s and a cloud that gave up at
1139
+ * 15.0 s, agreeing only by accident, with no way to tell whose deadline a
1140
+ * caller had actually hit.
1141
+ */
1142
+ patience_ms: import_zod7.z.number().int().min(MIN_PATIENCE_MS).max(MAX_PATIENCE_MS)
1143
+ });
1144
+ var cloudCancel = import_zod7.z.object({
1145
+ type: import_zod7.z.literal("cancel"),
1146
+ slug,
1147
+ job_id: import_zod7.z.uuid().nullable()
1148
+ });
1149
+ var cloudPublish = import_zod7.z.object({
1150
+ type: import_zod7.z.literal("publish"),
1151
+ slug,
1152
+ /**
1153
+ * Flat and keyed by `parameterSpec.name`, exactly like `cloudInvoke.params`
1154
+ * — a publisher carries parameter specs and the same §4.4 validation, so it
1155
+ * must carry the same shape. Note this is *not* the shape of
1156
+ * `publisherConfig.failsafe`, which is a complete nested ROS message: the
1157
+ * failsafe is authored once by the developer against the type, never sent
1158
+ * by a caller and never rule-checked per field.
1159
+ */
1160
+ message: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.unknown())
1161
+ });
1162
+ var bridgeJobUpdate = import_zod7.z.object({
1163
+ type: import_zod7.z.literal("job_update"),
1164
+ job_id: import_zod7.z.uuid(),
1165
+ slug,
1166
+ state: jobState,
1167
+ feedback: import_zod7.z.unknown().nullable(),
1168
+ progress: import_zod7.z.number().min(0).max(1).nullable(),
1169
+ result: import_zod7.z.unknown().nullable(),
1170
+ /** Same shape as `job.error`, `details` included — see `jobs.ts`. */
1171
+ error: import_zod7.z.object({ code: import_zod7.z.string().min(1), message: import_zod7.z.string().min(1), details: import_zod7.z.unknown().optional() }).nullable(),
1172
+ timestamp_ms: import_zod7.z.number().int().nonnegative()
1173
+ });
1174
+ var bridgeJobLost = import_zod7.z.object({
1175
+ type: import_zod7.z.literal("job_lost"),
1176
+ job_ids: import_zod7.z.array(import_zod7.z.uuid())
1177
+ });
1178
+ var cloudIntrospectRequest = import_zod7.z.object({
1179
+ type: import_zod7.z.literal("introspect_request"),
1180
+ request_id: import_zod7.z.string().min(1).max(64)
1181
+ });
1182
+ var bridgeIntrospect = import_zod7.z.object({
1183
+ type: import_zod7.z.literal("introspect"),
1184
+ request_id: import_zod7.z.string().min(1).max(64),
1185
+ graph: rosGraph
1186
+ });
1187
+ var cloudTypeRequest = import_zod7.z.object({
1188
+ type: import_zod7.z.literal("type_request"),
1189
+ request_id: import_zod7.z.string().min(1).max(64),
1190
+ type_names: import_zod7.z.array(rosTypeName).min(1).max(50)
1191
+ });
1192
+ var bridgeTypeDefinitions = import_zod7.z.object({
1193
+ type: import_zod7.z.literal("type_definitions"),
1194
+ request_id: import_zod7.z.string().min(1).max(64),
1195
+ definitions: import_zod7.z.array(typeDefinition),
1196
+ unresolved: import_zod7.z.array(import_zod7.z.string())
1197
+ });
1198
+ var bridgeState = import_zod7.z.object({
1199
+ online: import_zod7.z.boolean(),
1200
+ latency_ms: import_zod7.z.number().nonnegative().nullable()
1201
+ });
1202
+ var snapshotHeader = import_zod7.z.object({
1203
+ type: import_zod7.z.literal("snapshot"),
1204
+ slug,
1205
+ /** `image/jpeg` in practice; stated so nothing has to sniff the bytes. */
1206
+ mime: import_zod7.z.string().min(1).max(64),
1207
+ width: import_zod7.z.number().int().positive(),
1208
+ height: import_zod7.z.number().int().positive(),
1209
+ timestamp_ms: import_zod7.z.number().int().nonnegative()
1210
+ });
1211
+ var cloudCameraStart = import_zod7.z.object({
1212
+ type: import_zod7.z.literal("camera_start"),
1213
+ slug,
1214
+ url: import_zod7.z.string().min(1),
1215
+ room: import_zod7.z.string().min(1),
1216
+ token: import_zod7.z.string().min(1),
1217
+ /**
1218
+ * Names **this attempt** (W6b), and is echoed in the `camera_state` that
1219
+ * answers it.
1220
+ *
1221
+ * W6a gave `camera_state` a `cause` and said in the same comment that a
1222
+ * cause is not a correlation. This is the other half. Start a camera, have
1223
+ * it fail slowly, start it again: the first attempt's failure arrives while
1224
+ * the second is in flight, matches on slug, and resolves the attempt it
1225
+ * knows nothing about. The viewer is then told the running stream failed,
1226
+ * for a reason belonging to an attempt that is already over.
1227
+ */
1228
+ request_id: import_zod7.z.string().min(1).max(64)
1229
+ });
1230
+ var bridgeAssetsAvailable = import_zod7.z.object({
1231
+ type: import_zod7.z.literal("assets_available"),
1232
+ /** Whether `/robot_description` (or the configured source) yielded a URDF. */
1233
+ urdf: import_zod7.z.boolean(),
1234
+ /**
1235
+ * Every `package://` URI the URDF references, verbatim and unresolved —
1236
+ * including the ones this bridge cannot find in its workspace. Reporting
1237
+ * only the resolvable ones would make an incomplete workspace look like a
1238
+ * complete robot, and the cloud would have nothing to show as missing.
1239
+ */
1240
+ meshes: import_zod7.z.array(import_zod7.z.string().min(1))
1241
+ });
1242
+ var cloudAssetRequest = import_zod7.z.object({
1243
+ type: import_zod7.z.literal("asset_request"),
1244
+ sync_id: import_zod7.z.uuid(),
1245
+ upload_url: import_zod7.z.url(),
1246
+ token: import_zod7.z.string().min(1),
1247
+ /** Which URIs to send. Empty means the URDF only. */
1248
+ meshes: import_zod7.z.array(import_zod7.z.string().min(1))
1249
+ });
1250
+ var bridgeAssetProgress = import_zod7.z.object({
1251
+ type: import_zod7.z.literal("asset_progress"),
1252
+ sync_id: import_zod7.z.uuid(),
1253
+ done: import_zod7.z.number().int().nonnegative(),
1254
+ total: import_zod7.z.number().int().nonnegative(),
1255
+ /**
1256
+ * **Each entry says why** — see `assetFailure` in `assets.ts` for the three
1257
+ * kinds and why one word was not enough. The bound is `assets.ts`'s too: a
1258
+ * `.dae` with 17,331 unresolvable internal references produced a frame 32
1259
+ * bytes over `MAX_WS_PAYLOAD_BYTES`, and `ws` enforces that **before**
1260
+ * delivery — so the outcome was the robot's own socket closed, mid-sync, by
1261
+ * a file in its workspace (Kassandra-W7a). A producer at its own ceiling
1262
+ * reports **one** `refused` entry naming the file, not one per reference.
1263
+ */
1264
+ failed: import_zod7.z.array(assetFailure).max(1e3),
1265
+ /**
1266
+ * Three values, because a boolean `finished` had nowhere to put a refusal.
1267
+ *
1268
+ * A second `asset_request` arriving while one is in flight has to be
1269
+ * answered with something. The bridge's guard is a backstop — the cloud owns
1270
+ * sync lifecycle and refuses a concurrent one first — but a backstop that
1271
+ * answers with silence is a backstop nobody can debug, and the alternative
1272
+ * on the table was to report every requested URI in `failed`. That would
1273
+ * have made `failed` mean two different things at once — *could not be
1274
+ * resolved* and *was never attempted* — which is the one-field-two-facts
1275
+ * defect this project has now split five times (`set`/`readable`,
1276
+ * `truncated`/`truncated_by`, `value`/`sample_count`, `publishing`/`cause`,
1277
+ * and camera health's own).
1278
+ *
1279
+ * So: `running` while work is happening, `finished` when the bridge will
1280
+ * send no more for this sync, `refused_busy` when it never started because
1281
+ * another sync was in flight. `failed` keeps its single meaning.
1282
+ *
1283
+ * Raised by Rosie-W7, who found the gap by asking what a second request
1284
+ * should do rather than picking the silent option.
1285
+ */
1286
+ state: import_zod7.z.enum(["running", "finished", "refused_busy"])
1287
+ });
1288
+ var cloudCameraStop = import_zod7.z.object({
1289
+ type: import_zod7.z.literal("camera_stop"),
1290
+ slug,
1291
+ /** Names this stop, echoed by the `camera_state` that answers it — see `cloudCameraStart.request_id`. */
1292
+ request_id: import_zod7.z.string().min(1).max(64)
1293
+ });
1294
+ var bridgeCameraState = import_zod7.z.object({
1295
+ type: import_zod7.z.literal("camera_state"),
1296
+ slug,
1297
+ publishing: import_zod7.z.boolean(),
1298
+ error: import_zod7.z.object({ code: import_zod7.z.string().min(1), message: import_zod7.z.string().min(1) }).nullable(),
1299
+ /**
1300
+ * Why this frame was sent (W6a).
1301
+ *
1302
+ * Without it, `{publishing: false, error: null}` is sent for **three
1303
+ * different things** — an answer to `camera_stop`, a stream stopped by a
1304
+ * configuration change, and a source that recovered — and the cloud can
1305
+ * only tell them apart by remembering what it saw before. Deriving a cause
1306
+ * from remembered state is precisely the inference this project keeps
1307
+ * finding to be wrong, and W6a exists because four failures had been
1308
+ * sharing one silence.
1309
+ *
1310
+ * `'command'` this frame answers a `camera_start` / `camera_stop`.
1311
+ * `'source'` unsolicited: the source's own health changed, whether or
1312
+ * not anybody is watching. This is the frame that makes a
1313
+ * wrong password visible without a viewer.
1314
+ * `'config_change'` a configuration change stopped this stream. Not a
1315
+ * failure, and it must not be logged as one.
1316
+ * `'live_lost'` publishing ended unexpectedly after it had started.
1317
+ *
1318
+ * Note it does **not** answer "which attempt is this?" — `camera_state`
1319
+ * still has no request id, and that remains a named deferral in cluster C.
1320
+ * `cause` says what kind of event this is; correlation is a separate fact
1321
+ * and giving one field both jobs would be the same mistake again.
1322
+ *
1323
+ * Required, not optional: an absent cause would default to the reading
1324
+ * somebody happens to assume, and every frame's sender knows its own
1325
+ * reason. Old bridges fail validation on this frame — acceptable while
1326
+ * nothing is deployed, and W8 is the first deployment.
1327
+ */
1328
+ cause: import_zod7.z.enum(["command", "source", "config_change", "live_lost"]),
1329
+ /**
1330
+ * When the **robot** observed this state — bridge capture time, never
1331
+ * receive time, the same discipline `timestamp_ms` follows for samples
1332
+ * (spec §6.3).
1333
+ *
1334
+ * It exists because the cloud stamped `resourceHealthState.changed_at_ms`
1335
+ * with its own `Date.now()`, and a **restatement** is by definition an old
1336
+ * state re-sent into an empty map. So after a cloud restart every failure —
1337
+ * including one from yesterday — was dated to the restart, in the one
1338
+ * scenario `changed_at_ms`'s own doc comment was written for: *"a page that
1339
+ * loads late must be able to tell a failure from a minute ago from one from
1340
+ * yesterday"*.
1341
+ *
1342
+ * On a restatement this carries **when the state was first observed**, not
1343
+ * when the frame was sent. A bridge that re-states a failure it has held for
1344
+ * an hour says so.
1345
+ */
1346
+ observed_at_ms: import_zod7.z.number().int().nonnegative(),
1347
+ /**
1348
+ * Which request this frame answers (W6b), or `null` when it answers none.
1349
+ *
1350
+ * `null` is not a gap and must not be treated as one: a `cause: 'source'`
1351
+ * frame — the unsolicited health report that makes a wrong password visible
1352
+ * with nobody watching — answers no request by definition, and so does a
1353
+ * `config_change` stop. Those are the majority of frames on a healthy
1354
+ * system.
1355
+ *
1356
+ * A frame with `cause: 'command'` carries the `request_id` of the
1357
+ * `camera_start` or `camera_stop` it answers. **The cloud resolves a
1358
+ * pending attempt only on a matching id**, and drops a `command` frame
1359
+ * whose id it no longer recognises rather than applying it to whatever is
1360
+ * pending — a late answer to a cancelled attempt is stale, not current.
1361
+ *
1362
+ * **The pairing rule is not in this schema, deliberately.** "Non-null iff
1363
+ * `cause === 'command'`" is a cross-field constraint; a zod `.refine()`
1364
+ * would express it at runtime and then **disappear** from the generated
1365
+ * JSON Schema, which is what the bridge vendors. The cloud would reject
1366
+ * frames the bridge had validated as correct — the same artifact/runtime
1367
+ * divergence that `.default()` publishing as `required` has produced four
1368
+ * times in this project, only pointing the other way. The rule is enforced
1369
+ * where the correlation is used, in the cloud's bridge frame handler, and
1370
+ * stated here so nobody has to derive it from that code.
1371
+ */
1372
+ request_id: import_zod7.z.string().min(1).max(64).nullable()
1373
+ });
1374
+
1375
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/rest.js
1376
+ var import_zod8 = require("zod");
1377
+ var robot = import_zod8.z.object({
1378
+ id: import_zod8.z.uuid(),
1379
+ name: import_zod8.z.string().min(1).max(63),
1380
+ created_at: import_zod8.z.iso.datetime()
1381
+ });
1382
+ var createRobotRequest = import_zod8.z.object({
1383
+ name: import_zod8.z.string().min(1).max(63)
1384
+ });
1385
+ var robotToken = import_zod8.z.string().regex(/^frt_[0-9a-f]{32}$/);
1386
+ var createRobotResponse = import_zod8.z.object({
1387
+ robot,
1388
+ token: robotToken
1389
+ });
1390
+ var robotListItem = import_zod8.z.object({
1391
+ ...robot.shape,
1392
+ bridge_state: bridgeState
1393
+ });
1394
+ var robotListResponse = import_zod8.z.object({
1395
+ robots: import_zod8.z.array(robotListItem)
1396
+ });
1397
+ var datapointValue = import_zod8.z.object({
1398
+ slug,
1399
+ value: import_zod8.z.unknown(),
1400
+ timestamp_ms: import_zod8.z.number().int().nonnegative()
1401
+ });
1402
+ var robotDetailResponse = import_zod8.z.object({
1403
+ ...robotListItem.shape,
1404
+ bridge_version: import_zod8.z.string().min(1).nullable(),
1405
+ last_hello_error: import_zod8.z.object({
1406
+ code: import_zod8.z.string().min(1),
1407
+ message: import_zod8.z.string().min(1),
1408
+ at: import_zod8.z.iso.datetime()
1409
+ }).nullable(),
1410
+ config: configState
1411
+ });
1412
+ var configDraftResponse = import_zod8.z.object({
1413
+ doc: robotConfigDoc,
1414
+ updated_at: import_zod8.z.iso.datetime().nullable(),
1415
+ issues: import_zod8.z.array(validationIssue)
1416
+ });
1417
+ var putConfigDraftRequest = import_zod8.z.object({ doc: robotConfigDoc });
1418
+ var publishConfigResponse = import_zod8.z.object({
1419
+ version: import_zod8.z.number().int().positive(),
1420
+ published_at: import_zod8.z.iso.datetime()
1421
+ });
1422
+ var configVersionsResponse = import_zod8.z.object({
1423
+ versions: import_zod8.z.array(import_zod8.z.object({
1424
+ version: import_zod8.z.number().int().positive(),
1425
+ published_at: import_zod8.z.iso.datetime()
1426
+ }))
1427
+ });
1428
+ var configVersionResponse = import_zod8.z.object({
1429
+ version: import_zod8.z.number().int().positive(),
1430
+ published_at: import_zod8.z.iso.datetime(),
1431
+ doc: robotConfigDoc
1432
+ });
1433
+ var introspectionResponse = import_zod8.z.object({
1434
+ graph: rosGraph,
1435
+ fetched_at: import_zod8.z.iso.datetime(),
1436
+ stale: import_zod8.z.boolean()
1437
+ });
1438
+ var typesResponse = import_zod8.z.object({
1439
+ types: import_zod8.z.array(typeDefinition)
1440
+ });
1441
+ var fetchTypesRequest = import_zod8.z.object({
1442
+ type_names: import_zod8.z.array(rosTypeName).min(1).max(50)
1443
+ });
1444
+ var fetchTypesResponse = import_zod8.z.object({
1445
+ types: import_zod8.z.array(typeDefinition),
1446
+ unresolved: import_zod8.z.array(import_zod8.z.string())
1447
+ });
1448
+ var datapointDescriptor = import_zod8.z.object({
1449
+ slug,
1450
+ builtin: import_zod8.z.boolean(),
1451
+ unit: import_zod8.z.string().nullable(),
1452
+ range: datapointRange.nullable(),
1453
+ rate: datapointRate.nullable()
1454
+ });
1455
+ var datapointListResponse = import_zod8.z.object({
1456
+ datapoints: import_zod8.z.array(datapointDescriptor)
1457
+ });
1458
+ var robotDetailsDoc = import_zod8.z.record(import_zod8.z.string().regex(/^[a-z][a-z0-9_-]{0,63}$/), import_zod8.z.union([import_zod8.z.string().max(4096), import_zod8.z.number(), import_zod8.z.boolean(), import_zod8.z.array(import_zod8.z.unknown()), import_zod8.z.record(import_zod8.z.string(), import_zod8.z.unknown())]));
1459
+ var putRobotDetailsRequest = import_zod8.z.object({ details: robotDetailsDoc });
1460
+ var invokeRequest = import_zod8.z.object({
1461
+ params: import_zod8.z.record(import_zod8.z.string(), import_zod8.z.unknown()),
1462
+ /**
1463
+ * How long **this call** is worth waiting for, in milliseconds (W6b).
1464
+ *
1465
+ * **Absent means `DEFAULT_PATIENCE_MS`** — today's behaviour, unchanged, for
1466
+ * every caller who does not care. It is optional because most callers have
1467
+ * no opinion, and forcing one on them would mean every SDK example carries a
1468
+ * number its author guessed.
1469
+ *
1470
+ * It exists because patience was a **server constant** and could therefore
1471
+ * only ever be wrong in one of two directions at a time: long enough for a
1472
+ * planner meant a dead service also took that long to report, and short
1473
+ * enough for a snappy lookup meant a legitimate slow job was reported as
1474
+ * `bridge_timeout` — a healthy robot, described as broken, with nothing the
1475
+ * caller could do about it.
1476
+ *
1477
+ * The number travels with the call to the bridge (`cloudInvoke.patience_ms`)
1478
+ * so that **one** deadline governs both sides. Capped at
1479
+ * `MAX_PATIENCE_MS`; above that the call is refused with
1480
+ * `validation_error` rather than silently clamped, because a caller who
1481
+ * asked for ten minutes and was quietly given two would read the timeout as
1482
+ * the robot's failure.
1483
+ *
1484
+ * For a service call this is the whole wait. For an action it bounds goal
1485
+ * *acceptance* — once a goal is accepted the job runs as long as it runs,
1486
+ * and is observed, not awaited.
1487
+ */
1488
+ patience_ms: import_zod8.z.number().int().min(MIN_PATIENCE_MS).max(MAX_PATIENCE_MS).optional()
1489
+ });
1490
+ var cancelRequest = import_zod8.z.object({
1491
+ job_id: import_zod8.z.uuid().nullable().optional()
1492
+ }).strict();
1493
+ var releaseLiveQuery = import_zod8.z.object({
1494
+ session_id: import_zod8.z.uuid().optional()
1495
+ }).strict();
1496
+ var invokeResponse = import_zod8.z.object({
1497
+ job,
1498
+ /** The slug's kind — see `commandResult.kind` for why the caller needs it. */
1499
+ kind: import_zod8.z.enum(["action", "service"])
1500
+ });
1501
+ var serviceCallResponse = import_zod8.z.object({
1502
+ result: import_zod8.z.unknown()
1503
+ });
1504
+ var publishRequest = import_zod8.z.object({
1505
+ message: import_zod8.z.record(import_zod8.z.string(), import_zod8.z.unknown())
1506
+ });
1507
+ var jobResponse = import_zod8.z.object({ job: job.nullable() });
1508
+ var rateLimitDetails = import_zod8.z.object({
1509
+ retry_after_ms: import_zod8.z.number().int().nonnegative()
1510
+ });
1511
+ var robotJobsResponse = import_zod8.z.object({
1512
+ jobs: import_zod8.z.array(job)
1513
+ });
1514
+ var exposure = import_zod8.z.object({
1515
+ slug,
1516
+ kind: import_zod8.z.enum(["datapoint", "action", "service", "publisher", "camera"]),
1517
+ builtin: import_zod8.z.boolean()
1518
+ });
1519
+ var exposureListResponse = import_zod8.z.object({
1520
+ exposures: import_zod8.z.array(exposure)
1521
+ });
1522
+ var SNAPSHOT_HEADERS = {
1523
+ ageMs: "x-fleetless-age-ms",
1524
+ timestampMs: "x-fleetless-timestamp-ms",
1525
+ width: "x-fleetless-width",
1526
+ height: "x-fleetless-height"
1527
+ };
1528
+ var cameraDescriptor = import_zod8.z.object({
1529
+ slug,
1530
+ width: import_zod8.z.number().int().positive(),
1531
+ height: import_zod8.z.number().int().positive(),
1532
+ fps: import_zod8.z.number().int().positive(),
1533
+ snapshot_interval_ms: import_zod8.z.number().int().positive()
1534
+ });
1535
+ var cameraListResponse = import_zod8.z.object({ cameras: import_zod8.z.array(cameraDescriptor) });
1536
+ var liveSessionResponse = import_zod8.z.object({
1537
+ /**
1538
+ * This viewer's hold, and the **only** thing `DELETE` should be given
1539
+ * (W6b).
1540
+ *
1541
+ * A hold was addressed by `{identity, robot, slug}` and nothing else, so
1542
+ * two tabs of one logged-in user were one hold as far as the refcount could
1543
+ * see. Closing either tab released it: the second tab kept its LiveKit
1544
+ * connection — the token is checked at join and never again — and went on
1545
+ * rendering a video that the robot had already stopped producing. The
1546
+ * viewer sees a frozen picture, not an ended session, which is the failure
1547
+ * this project rejects everywhere else.
1548
+ *
1549
+ * `DELETE` without a session id keeps today's meaning — *release my holds
1550
+ * on this camera* — because an SDK that has lost its id, or a client that
1551
+ * is going away entirely, still needs a way to let go. It is the blunt
1552
+ * form, and it is the one that strands other tabs; new callers pass the id.
1553
+ */
1554
+ session_id: import_zod8.z.uuid(),
1555
+ url: import_zod8.z.string().min(1),
1556
+ room: import_zod8.z.string().min(1),
1557
+ token: import_zod8.z.string().min(1),
1558
+ expires_at: import_zod8.z.iso.datetime()
1559
+ });
1560
+ var snapshotMetaResponse = import_zod8.z.object({
1561
+ slug,
1562
+ timestamp_ms: import_zod8.z.number().int().nonnegative().nullable(),
1563
+ age_ms: import_zod8.z.number().int().nonnegative().nullable(),
1564
+ width: import_zod8.z.number().int().positive().nullable(),
1565
+ height: import_zod8.z.number().int().positive().nullable(),
1566
+ mime: import_zod8.z.string().nullable()
1567
+ });
1568
+ var historyQuery = import_zod8.z.object({
1569
+ from: import_zod8.z.string().min(1).max(32),
1570
+ /** Defaults to now. */
1571
+ to: import_zod8.z.string().min(1).max(32).optional(),
1572
+ /** Bucket width, e.g. `10s`, `1m`. Absent means raw samples. */
1573
+ window: import_zod8.z.string().min(2).max(16).optional(),
1574
+ agg: import_zod8.z.enum(["min", "max", "avg"]).optional(),
1575
+ /** A numeric field inside an object value, e.g. `pose.x` (§4.4 paths). */
1576
+ field: import_zod8.z.string().min(1).max(128).optional(),
1577
+ /**
1578
+ * `z.coerce` because this schema describes a **query string**, where every
1579
+ * value arrives as text. A bare `z.number()` would make each route coerce
1580
+ * `limit` by hand before parsing — Nimbus had to, and flagged that the next
1581
+ * query-taking route would have to as well. A schema that does not match
1582
+ * the wire it describes exports its problem to every consumer.
1583
+ */
1584
+ limit: import_zod8.z.coerce.number().int().positive().max(1e4).optional()
1585
+ });
1586
+ var historySamplesResponse = import_zod8.z.object({
1587
+ slug,
1588
+ kind: import_zod8.z.literal("samples"),
1589
+ samples: import_zod8.z.array(import_zod8.z.object({ timestamp_ms: import_zod8.z.number().int().nonnegative(), value: import_zod8.z.unknown() })),
1590
+ truncated: import_zod8.z.boolean(),
1591
+ /**
1592
+ * Why it was cut, `null` when it was not — because the two causes have
1593
+ * **different remedies** and a single boolean cannot tell them apart:
1594
+ *
1595
+ * `'limit'` too many rows. Raise `limit` (up to 10 000).
1596
+ * `'bytes'` the rows are large. Raising `limit` will not help — narrow
1597
+ * the range, or name a numeric `field` so whole messages are
1598
+ * not carried.
1599
+ *
1600
+ * A caller cannot derive this: comparing `samples.length` against `limit`
1601
+ * only works if they sent one, and the server's default is not in the
1602
+ * response. So without this field, "raise the limit" is the natural next
1603
+ * move in both cases, and in the second it changes nothing.
1604
+ *
1605
+ * Nullable rather than optional on purpose: `.default()` publishes as
1606
+ * `required` in the JSON Schema artifacts, which is the contradiction this
1607
+ * project has now hit five times.
1608
+ */
1609
+ truncated_by: import_zod8.z.enum(["limit", "bytes"]).nullable()
1610
+ });
1611
+ var historyBucketsResponse = import_zod8.z.object({
1612
+ slug,
1613
+ kind: import_zod8.z.literal("buckets"),
1614
+ window_ms: import_zod8.z.number().int().positive(),
1615
+ agg: import_zod8.z.enum(["min", "max", "avg"]),
1616
+ buckets: import_zod8.z.array(import_zod8.z.object({
1617
+ bucket_start_ms: import_zod8.z.number().int().nonnegative(),
1618
+ /**
1619
+ * The aggregate over this bucket's **numeric** samples — or `null` when
1620
+ * none of them were numeric, which is **not** the same as the bucket
1621
+ * being empty. `sample_count` is the field that separates those facts:
1622
+ *
1623
+ * value: null, sample_count: 0 nothing was recorded — a gap
1624
+ * value: null, sample_count: 3 three samples, none of them numeric
1625
+ * value: 0, sample_count: 3 three samples, and the average is zero
1626
+ *
1627
+ * A chart must draw the first as a break in the line and must **not**
1628
+ * draw the second as one: data exists there, it simply has no height.
1629
+ *
1630
+ * This sentence previously read "`null` only ever means 'no samples in
1631
+ * this bucket'", and the implementation counted numeric contributors,
1632
+ * so the second row above was indistinguishable from the first and the
1633
+ * console rendered "empty — no samples" over live data.
1634
+ */
1635
+ value: import_zod8.z.number().nullable(),
1636
+ /**
1637
+ * Every sample that landed in this bucket and inside the queried range,
1638
+ * whether or not it contributed to `value` — which is the point of the
1639
+ * field, since only a count of *all* samples can prove a bucket empty
1640
+ * rather than merely unplottable.
1641
+ *
1642
+ * Two consequences, stated rather than left to be discovered:
1643
+ *
1644
+ * - `value` is not an average *of* `sample_count` samples when a
1645
+ * datapoint's values are mixed, so **`value * sample_count` is not a
1646
+ * sum**.
1647
+ * - On a first or last bucket the count reflects the **range**, not the
1648
+ * bucket: an edge bucket can begin before `from` or extend past `to`,
1649
+ * and only in-range samples are counted. A low edge count is a
1650
+ * boundary effect, not a quiet period.
1651
+ */
1652
+ sample_count: import_zod8.z.number().int().nonnegative()
1653
+ }))
1654
+ });
1655
+ var robotDeletionSummary = import_zod8.z.object({
1656
+ /**
1657
+ * Datapoints, actions, services and publishers in the **published**
1658
+ * configuration — what the robot was actually running. **Cameras are not
1659
+ * counted here**; they are the `cameras` array below.
1660
+ *
1661
+ * The split has to be stated because the summary carries both, and the
1662
+ * console renders them in one sentence: *"this deletes N published slugs …
1663
+ * and M cameras"*. With cameras inside `slug_count` that sentence counts
1664
+ * them twice, on the one screen whose whole justification is naming what an
1665
+ * irreversible click destroys (Momus, W6a review — the cloud summed all
1666
+ * five and the console then added the cameras again).
1667
+ *
1668
+ * A draft is destroyed too and is described by `had_unpublished_draft`
1669
+ * rather than by either of these: describing three things with two numbers
1670
+ * would make each of them mean something else.
1671
+ */
1672
+ slug_count: import_zod8.z.number().int().nonnegative(),
1673
+ sample_rows: import_zod8.z.number().int().nonnegative(),
1674
+ bytes_freed: import_zod8.z.number().int().nonnegative(),
1675
+ cameras: import_zod8.z.array(slug),
1676
+ /**
1677
+ * Assets destroyed with the robot (W7), and **`asset_bytes_freed` is what
1678
+ * this org actually gets back** — not the sum of the assets' sizes.
1679
+ *
1680
+ * Storage is content-addressed, so a mesh two robots share survives the
1681
+ * deletion of one of them and frees nothing. Reporting the total would tell
1682
+ * a developer they are about to recover 400 MB and hand back 4, on the one
1683
+ * screen whose entire justification is naming what an irreversible click
1684
+ * destroys. Same reasoning that keeps `cameras` out of `slug_count`: this
1685
+ * summary is read aloud to a human, and a number that is nearly right is
1686
+ * worse here than an absent one.
1687
+ *
1688
+ * `asset_count` is the plain count of the robot's asset rows, all of which
1689
+ * do go away.
1690
+ */
1691
+ asset_count: import_zod8.z.number().int().nonnegative(),
1692
+ asset_bytes_freed: import_zod8.z.number().int().nonnegative(),
1693
+ had_live_session: import_zod8.z.boolean(),
1694
+ /**
1695
+ * Whether an unpublished draft went with it — separately, because the
1696
+ * counts above deliberately do not include it and a record that silently
1697
+ * omitted the draft would be a receipt for less than was destroyed.
1698
+ *
1699
+ * `true` also covers the robot that was configured but never published:
1700
+ * there the counts are zero and this is the only field saying anything
1701
+ * was there at all.
1702
+ */
1703
+ had_unpublished_draft: import_zod8.z.boolean()
1704
+ });
1705
+ var RESOURCE_HEALTH_STATES = [
1706
+ "ok",
1707
+ /** The host did not answer. Not the same as refusing the password. */
1708
+ "unreachable",
1709
+ /** The host answered and rejected the credentials. */
1710
+ "auth_failed",
1711
+ /** The stored password cannot be decrypted — see `credentialSummary.readable`. */
1712
+ "unreadable_credential",
1713
+ /**
1714
+ * A camera names a credential that **does not exist** in this org — deleted,
1715
+ * mistyped, or belonging to somebody else (W6a review).
1716
+ *
1717
+ * Separate from `unreadable_credential` because that one asserts a
1718
+ * decryption that was attempted and failed, and here nothing was ever
1719
+ * encrypted: the developer is sent to a page where the credential is not
1720
+ * listed at all, to rotate something that is not there. And separate from
1721
+ * `unknown`, which means "the robot reported a failure we cannot classify"
1722
+ * — a different fact with a different fix.
1723
+ *
1724
+ * It is reachable by a typo: `cloud-config-frame.ts` deliberately tolerates
1725
+ * an unresolved `credentials_ref` at publish time, so this is an ordinary
1726
+ * developer mistake rather than an edge case.
1727
+ */
1728
+ "credential_missing",
1729
+ /** A configuration change stopped this stream, deliberately. */
1730
+ "stopped_by_config_change",
1731
+ /** Publishing failed after the session was already granted. */
1732
+ "publish_failed",
1733
+ /**
1734
+ * Something is wrong and this platform cannot say what.
1735
+ *
1736
+ * The alternative was worse. A bridge error code the mapping table does not
1737
+ * know had two possible fallbacks: report `ok`, which hides a real failure,
1738
+ * or fold it into `unreachable`, which **asserts a cause nobody
1739
+ * established** — sending a developer to check a network when the problem
1740
+ * may be a password. The map falls back here and logs the unmapped code
1741
+ * loudly, so the gap in the table is visible instead of confident.
1742
+ */
1743
+ "unknown"
1744
+ ];
1745
+ var resourceHealthState = import_zod8.z.object({
1746
+ robot_id: import_zod8.z.uuid(),
1747
+ kind: import_zod8.z.enum(["camera", "credential"]),
1748
+ /** The camera slug, or the credential name. */
1749
+ ref: import_zod8.z.string().min(1).max(64),
1750
+ state: import_zod8.z.enum(RESOURCE_HEALTH_STATES),
1751
+ /** A short human-readable reason, or `null`. Never an exception message. */
1752
+ reason: import_zod8.z.string().max(200).nullable(),
1753
+ /**
1754
+ * When this state was entered — not when it was sent. A page that loads
1755
+ * late must be able to tell a failure from a minute ago from one from
1756
+ * yesterday, and a state with only a send time cannot.
1757
+ */
1758
+ changed_at_ms: import_zod8.z.number().int().nonnegative()
1759
+ });
1760
+ var resourceHealthListResponse = import_zod8.z.object({
1761
+ resources: import_zod8.z.array(resourceHealthState)
1762
+ });
1763
+ var orgQuotas = import_zod8.z.object({
1764
+ max_robots: import_zod8.z.number().int().positive(),
1765
+ max_apps: import_zod8.z.number().int().positive(),
1766
+ max_end_users: import_zod8.z.number().int().positive(),
1767
+ max_retention_bytes: import_zod8.z.number().int().nonnegative(),
1768
+ max_retention_writes_per_minute: import_zod8.z.number().int().nonnegative(),
1769
+ max_realtime_connections: import_zod8.z.number().int().positive(),
1770
+ /**
1771
+ * Asset storage (§4.6, W7) — **its own dial, not part of
1772
+ * `max_retention_bytes`.** A sync grows storage in jumps and time series
1773
+ * grow steadily; one dial would let the first crowd out the second, and the
1774
+ * org that hit its limit would be told to look at the wrong thing.
1775
+ *
1776
+ * **Counted per distinct blob *this org references* — not per asset row, and
1777
+ * not per object the platform stores on its behalf (W7a, D1).** The two
1778
+ * readings are indistinguishable from the number alone and a customer is
1779
+ * entitled to know which one they are being charged for.
1780
+ *
1781
+ * Within an org, sharing is free: two robots referencing the same mesh cost
1782
+ * one copy, which is what dedup means to a customer, and anything else
1783
+ * charges an org twice for a fleet of identical robots — the normal case.
1784
+ *
1785
+ * **Across orgs, sharing is not free, and W7 shipped the opposite.** Storage
1786
+ * stays globally content-addressed (one object per sha256; that efficiency
1787
+ * is real), but accounting is per-org: an org is charged for each distinct
1788
+ * blob it references and credited when its own last reference goes, whether
1789
+ * or not the blob survives for somebody else. Global refcounting made the
1790
+ * first org to sync a blob pay for it forever while every later org stored
1791
+ * it free — so the quota was evadable by anyone whose mesh someone else had
1792
+ * already uploaded, and an org's own number depended on who got there first,
1793
+ * which nobody can predict. Measured before the change: 342 bytes held by an
1794
+ * org owning no assets, with no operation able to free them.
1795
+ */
1796
+ max_asset_storage_bytes: import_zod8.z.number().int().nonnegative()
1797
+ });
1798
+ var orgQuotaUsageCounts = import_zod8.z.object({
1799
+ max_robots: import_zod8.z.number().int().nonnegative(),
1800
+ max_apps: import_zod8.z.number().int().nonnegative(),
1801
+ max_end_users: import_zod8.z.number().int().nonnegative(),
1802
+ max_retention_bytes: import_zod8.z.number().int().nonnegative(),
1803
+ max_asset_storage_bytes: import_zod8.z.number().int().nonnegative(),
1804
+ max_retention_writes_per_minute: import_zod8.z.number().int().nonnegative(),
1805
+ max_realtime_connections: import_zod8.z.number().int().nonnegative()
1806
+ }).partial();
1807
+ var orgQuotaUsage = import_zod8.z.object({ quotas: orgQuotas, usage: orgQuotaUsageCounts });
1808
+ var credentialSummary = import_zod8.z.object({
1809
+ name: import_zod8.z.string().min(1).max(64),
1810
+ username: import_zod8.z.string().nullable(),
1811
+ /**
1812
+ * Whether a password has ever been stored for this name.
1813
+ *
1814
+ * **Always `true` today**, and stated so rather than left to be inferred:
1815
+ * `credentialWriteRequest` requires a non-empty password, so no row can
1816
+ * exist without one, and both write paths set this literally. A consumer
1817
+ * branching on `set === false` is writing dead code — the SDK README
1818
+ * currently teaches exactly that (Momus, W6a review).
1819
+ *
1820
+ * The field is kept because the fact it names is the one `readable`
1821
+ * qualifies, and because a username-only credential is a plausible future
1822
+ * shape. If that never arrives, this should be removed rather than left as
1823
+ * a permanent constant wearing the costume of a question.
1824
+ */
1825
+ set: import_zod8.z.boolean(),
1826
+ /**
1827
+ * Whether that password can still be **decrypted** — a different fact from
1828
+ * `set`, and deliberately a second field rather than a tri-state on the
1829
+ * first (W6a).
1830
+ *
1831
+ * They come apart when `CAMERA_CREDENTIALS_KEY` is rotated, unset or wrong,
1832
+ * or when a row is corrupt. W6 made that survivable: one unreadable
1833
+ * credential costs the cameras that reference it instead of taking the
1834
+ * robot offline. But the surviving failure was **invisible** — this route
1835
+ * answered `set: true` with `used_by` naming the dependent camera, for a
1836
+ * credential that ships as `credentials: {}` on every config frame, and the
1837
+ * only evidence was a server log no developer can read.
1838
+ *
1839
+ * `set: true, readable: false` is therefore the shape that says "a password
1840
+ * is stored and this platform can no longer use it" — which is a thing to
1841
+ * act on, and nothing else in the API could say it.
1842
+ */
1843
+ readable: import_zod8.z.boolean(),
1844
+ used_by: import_zod8.z.array(import_zod8.z.object({ robot_id: import_zod8.z.uuid(), slug }))
1845
+ });
1846
+ var credentialListResponse = import_zod8.z.object({ credentials: import_zod8.z.array(credentialSummary) });
1847
+ var credentialWriteRequest = import_zod8.z.object({
1848
+ username: import_zod8.z.string().min(1).max(128),
1849
+ password: import_zod8.z.string().min(1).max(512)
1850
+ });
1851
+
1852
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/realtime.js
1853
+ var import_zod11 = require("zod");
1854
+
1855
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/client-auth.js
1856
+ var import_zod10 = require("zod");
1857
+
1858
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/apps.js
1859
+ var import_zod9 = require("zod");
1860
+ var appIdentifier = slug;
1861
+ var app = import_zod9.z.object({
1862
+ id: import_zod9.z.uuid(),
1863
+ org_id: import_zod9.z.uuid(),
1864
+ name: import_zod9.z.string().min(1).max(120),
1865
+ identifier: appIdentifier,
1866
+ /** Robots are referenced individually; tags never grant rights (§12.2). */
1867
+ robot_ids: import_zod9.z.array(import_zod9.z.uuid()),
1868
+ /**
1869
+ * Whether this app accepts **self-registering** OAuth clients (RFC 7591).
1870
+ *
1871
+ * Off by default and per app, because a normal app has no reason to accept
1872
+ * them: its own client is registered by the developer with known redirect
1873
+ * URIs. An MCP app does, because the end user *"trägt nur die URL ein"*
1874
+ * (§17) and the client registers itself — nobody vetted it, and
1875
+ * `POST /oauth/register` is therefore an **unauthenticated write endpoint**
1876
+ * standing in front of tools that move a physical robot.
1877
+ *
1878
+ * The flag is app state rather than a deployment setting so that turning it
1879
+ * on is a decision somebody made about one app, visible in the console and
1880
+ * in the audit log. It is also the precursor of W7c's MCP-App kind — the
1881
+ * model grows here rather than being retrofitted around it.
1882
+ */
1883
+ accepts_dynamic_clients: import_zod9.z.boolean(),
1884
+ /**
1885
+ * Whether this app serves a remote MCP server at `/mcp/<identifier>` (§17).
1886
+ *
1887
+ * **A switch, not an app kind.** §2 and §17 called the MCP app *"eine eigene
1888
+ * App-Art"*; André decided on 2026-08-18 that it is a per-app switch the
1889
+ * developer flips in the console, and §17 was reworded in the same wave
1890
+ * rather than left contradicting this field. The model grows here — which
1891
+ * is what the comment on `accepts_dynamic_clients` above predicted it would
1892
+ * do, one wave before there was anything to add.
1893
+ *
1894
+ * The two switches are related and not the same. `accepts_dynamic_clients`
1895
+ * decides whether a client may **register itself**; this one decides whether
1896
+ * there is anything for it to reach. An MCP app will usually want both,
1897
+ * because §17's end user *"trägt nur die URL ein"* and the AI tool registers
1898
+ * itself — but a developer who registers their own MCP client by hand wants
1899
+ * exactly this one, and coupling them would take that away.
1900
+ *
1901
+ * **Off means off at the metadata too.** With this false, `/mcp/<app>`
1902
+ * answers `404` and so do its discovery documents. A resource that is
1903
+ * advertised and not served sends a conforming client through the whole
1904
+ * discovery chain to a door that is not there — and W7b spent a wave making
1905
+ * that chain walkable.
1906
+ */
1907
+ mcp_enabled: import_zod9.z.boolean(),
1908
+ created_at: import_zod9.z.iso.datetime()
1909
+ });
1910
+ var createAppRequest = import_zod9.z.object({
1911
+ name: import_zod9.z.string().min(1).max(120),
1912
+ identifier: appIdentifier,
1913
+ robot_ids: import_zod9.z.array(import_zod9.z.uuid()).optional(),
1914
+ /** Optional, defaulting to `false` — same reasoning as `robot_ids` above: setting it at creation is the obvious operation, and refusing it here would make a `.strict()` request reject the field the caller can plainly see on `app`. */
1915
+ accepts_dynamic_clients: import_zod9.z.boolean().optional(),
1916
+ /**
1917
+ * **W7c's playbook said this field would not be accepted here, and the
1918
+ * sentence above is why that was wrong.** The argument for refusing it was
1919
+ * W7's `robot_ids` finding — a create shape that silently drops a field cost
1920
+ * two people a day each. But that finding was closed by *accepting* the
1921
+ * field, not by refusing it, and this request is `.strict()`: refusing
1922
+ * `mcp_enabled` would make it `400` on a field the caller can plainly see on
1923
+ * `app`, which is the exact shape the line above rejects. One rule, both
1924
+ * switches.
1925
+ */
1926
+ mcp_enabled: import_zod9.z.boolean().optional()
1927
+ }).strict();
1928
+ var updateAppRequest = import_zod9.z.object({
1929
+ name: import_zod9.z.string().min(1).max(120).optional(),
1930
+ robot_ids: import_zod9.z.array(import_zod9.z.uuid()).optional(),
1931
+ accepts_dynamic_clients: import_zod9.z.boolean().optional(),
1932
+ mcp_enabled: import_zod9.z.boolean().optional()
1933
+ });
1934
+ var serverKeyToken = import_zod9.z.string().regex(/^flk_[0-9a-f]{32}$/);
1935
+ var serverKey = import_zod9.z.object({
1936
+ id: import_zod9.z.uuid(),
1937
+ app_id: import_zod9.z.uuid(),
1938
+ name: import_zod9.z.string().min(1).max(120),
1939
+ created_at: import_zod9.z.iso.datetime(),
1940
+ /** Null until first use — the cheapest way to spot a key nobody needs. */
1941
+ last_used_at: import_zod9.z.iso.datetime().nullable()
1942
+ });
1943
+ var createServerKeyResponse = import_zod9.z.object({
1944
+ server_key: serverKey,
1945
+ key: serverKeyToken
1946
+ });
1947
+ var role = import_zod9.z.object({
1948
+ id: import_zod9.z.uuid(),
1949
+ app_id: import_zod9.z.uuid(),
1950
+ name: import_zod9.z.string().min(1).max(60),
1951
+ builtin: import_zod9.z.boolean()
1952
+ });
1953
+ var rolePermissions = import_zod9.z.object({
1954
+ role_id: import_zod9.z.uuid(),
1955
+ /**
1956
+ * **A slug is unique per robot across ALL service kinds** (spec §4.1:
1957
+ * "Jeder Dienst erhält einen Slug" — one namespace, not one per kind), and
1958
+ * the cloud's config validation enforces that with a kind-agnostic
1959
+ * collection pass. That is why this list carries slugs and not
1960
+ * (kind, slug) pairs: when W4 adds actions, services and publishers, a
1961
+ * grant keeps meaning exactly what it means today, and this shape does not
1962
+ * change. What W4 does need is an endpoint that lists every *grantable*
1963
+ * slug of a robot with its kind, so the console's matrix can offer them —
1964
+ * today it enumerates datapoints only, which is the seam that would
1965
+ * otherwise force a rebuild.
1966
+ */
1967
+ grants: import_zod9.z.array(import_zod9.z.object({
1968
+ robot_id: import_zod9.z.uuid(),
1969
+ slugs: import_zod9.z.array(slug)
1970
+ })),
1971
+ /**
1972
+ * App-wide abilities a role grants, as opposed to per-slug grants above.
1973
+ *
1974
+ * **A capability here is a promise, and two of them have not been kept.**
1975
+ * `action_history` and `presence` have been gated by this object since W4
1976
+ * and are implemented nowhere — no route, no SDK method, no realtime frame
1977
+ * (register row 8). A console can therefore switch them on and nothing
1978
+ * changes, which is worse than their absence: the developer believes they
1979
+ * granted something.
1980
+ *
1981
+ * `assets` (W7) must not become the third. It gates §4.6's asset store,
1982
+ * which is not covered by `grants` because **assets are not slugs** — and it
1983
+ * is its own decision rather than a side effect of reaching the robot,
1984
+ * because a mesh set gives away the machine's build.
1985
+ */
1986
+ capabilities: import_zod9.z.object({
1987
+ action_history: import_zod9.z.boolean(),
1988
+ presence: import_zod9.z.boolean(),
1989
+ assets: import_zod9.z.boolean()
1990
+ })
1991
+ });
1992
+ var appMembership = import_zod9.z.object({
1993
+ end_user_id: import_zod9.z.uuid(),
1994
+ app_id: import_zod9.z.uuid(),
1995
+ role_id: import_zod9.z.uuid()
1996
+ });
1997
+ var brandingConfig = import_zod9.z.object({
1998
+ /** `#rrggbb`, lowercase — one canonical spelling so two configs that look identical are identical. */
1999
+ primary_color: import_zod9.z.string().regex(/^#[0-9a-f]{6}$/, "primary_color must be lowercase #rrggbb"),
2000
+ /**
2001
+ * 256 KiB of raw image at most. Base64 costs 4 bytes per 3, so the encoded
2002
+ * ceiling is stated here in encoded characters — the unit the validator can
2003
+ * actually count, rather than one it would have to infer.
2004
+ */
2005
+ logo_data_uri: import_zod9.z.string().max(349528).regex(/^data:image\/(png|jpeg);base64,[A-Za-z0-9+/]+={0,2}$/, "logo must be a base64 data URI of image/png or image/jpeg").optional(),
2006
+ footer_text: import_zod9.z.string().min(1).max(200).optional()
2007
+ });
2008
+
2009
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/client-auth.js
2010
+ var clientLoginRequest = import_zod10.z.object({
2011
+ app_identifier: appIdentifier,
2012
+ email: import_zod10.z.email(),
2013
+ password: import_zod10.z.string().min(1)
2014
+ });
2015
+ var clientRefreshRequest = import_zod10.z.object({
2016
+ refresh_token: import_zod10.z.string().min(1)
2017
+ });
2018
+ var clientLogoutRequest = import_zod10.z.object({
2019
+ refresh_token: import_zod10.z.string().min(1)
2020
+ });
2021
+ var clientIdentity = import_zod10.z.object({
2022
+ kind: import_zod10.z.enum(["developer", "end_user", "server_key"]),
2023
+ developer_id: import_zod10.z.uuid().nullable(),
2024
+ end_user_id: import_zod10.z.uuid().nullable(),
2025
+ server_key_id: import_zod10.z.uuid().nullable(),
2026
+ app_id: import_zod10.z.uuid().nullable(),
2027
+ role_id: import_zod10.z.uuid().nullable(),
2028
+ email: import_zod10.z.email().nullable()
2029
+ });
2030
+
2031
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/realtime.js
2032
+ var clientAuth = import_zod11.z.object({
2033
+ type: import_zod11.z.literal("auth"),
2034
+ token: import_zod11.z.string().min(1)
2035
+ });
2036
+ var authOk = import_zod11.z.object({
2037
+ type: import_zod11.z.literal("auth_ok"),
2038
+ identity: clientIdentity
2039
+ });
2040
+ var authError = import_zod11.z.object({
2041
+ type: import_zod11.z.literal("auth_error"),
2042
+ code: import_zod11.z.string().min(1),
2043
+ message: import_zod11.z.string().min(1)
2044
+ });
2045
+ var clientInvoke = import_zod11.z.object({
2046
+ type: import_zod11.z.literal("invoke"),
2047
+ request_id: import_zod11.z.string().min(1).max(64),
2048
+ robot_id: import_zod11.z.uuid(),
2049
+ slug,
2050
+ /** Parameters by field path, validated against the config's rules (§4.4). */
2051
+ params: import_zod11.z.record(import_zod11.z.string(), import_zod11.z.unknown()),
2052
+ /**
2053
+ * How long this one call is worth waiting for (W6b) — the same field,
2054
+ * meaning and cap as `invokeRequest.patience_ms`; absent means
2055
+ * `DEFAULT_PATIENCE_MS`.
2056
+ *
2057
+ * It is here because **§11.1 parity is a rule, not a preference**: what REST
2058
+ * can do travels over this socket. The first version of this delta gave
2059
+ * `patience_ms` to the REST body only — and the SDK invokes exclusively over
2060
+ * the realtime channel, so the field would have been unreachable for every
2061
+ * SDK caller while appearing in the documentation. W6a shipped four SDK
2062
+ * methods no SDK caller could invoke; this is the same defect caught before
2063
+ * it shipped, by the SDK owner rather than by a reviewer.
2064
+ */
2065
+ patience_ms: import_zod11.z.number().int().min(MIN_PATIENCE_MS).max(MAX_PATIENCE_MS).optional()
2066
+ });
2067
+ var clientCancel = import_zod11.z.object({
2068
+ type: import_zod11.z.literal("cancel"),
2069
+ request_id: import_zod11.z.string().min(1).max(64),
2070
+ robot_id: import_zod11.z.uuid(),
2071
+ /** Which slug — required, and the only address a cancel had until W6b. */
2072
+ slug,
2073
+ /**
2074
+ * Which job on that slug (W6b), or `null` for *whatever is running there*.
2075
+ *
2076
+ * The two are different requests and both are legitimate. An operator
2077
+ * hitting a stop button means the second: stop the machine, whatever it is
2078
+ * doing. A client cancelling the job it started means the first — and until
2079
+ * this field existed it could not say so, so a cancel that arrived just
2080
+ * after its own job ended stopped the next caller's job instead. Same slug,
2081
+ * same wire frame, entirely different machine behaviour, and nothing in the
2082
+ * protocol able to tell them apart.
2083
+ *
2084
+ * A named id that is not running answers `not_found` rather than falling
2085
+ * back to the slug. Falling back would be the platform deciding that the
2086
+ * caller did not really mean the id they typed.
2087
+ */
2088
+ job_id: import_zod11.z.uuid().nullable()
2089
+ });
2090
+ var clientPublish = import_zod11.z.object({
2091
+ type: import_zod11.z.literal("publish"),
2092
+ request_id: import_zod11.z.string().min(1).max(64),
2093
+ robot_id: import_zod11.z.uuid(),
2094
+ slug,
2095
+ message: import_zod11.z.record(import_zod11.z.string(), import_zod11.z.unknown())
2096
+ });
2097
+ var commandResult = import_zod11.z.object({
2098
+ type: import_zod11.z.literal("command_result"),
2099
+ request_id: import_zod11.z.string().min(1).max(64),
2100
+ ok: import_zod11.z.boolean(),
2101
+ /**
2102
+ * The job this reply is *about* — which is not always the caller's own.
2103
+ *
2104
+ * - `ok:true` on an invoke or a call: the job that was just created.
2105
+ * - `ok:true` on a cancel: the job the cancel was sent to.
2106
+ * - `ok:false, code:'busy'`: **the job that is already running** — the
2107
+ * caller has none. This is the §11.3 "inkl. Information, was läuft", and
2108
+ * it is the whole reason a busy refusal is useful: the caller learns
2109
+ * whether to wait or to give up (see `busyDetails`).
2110
+ * - any other refusal: `null`.
2111
+ */
2112
+ job: job.nullable(),
2113
+ /**
2114
+ * The **kind** of the slug this command addressed, when the slug resolved.
2115
+ *
2116
+ * Invoke and call share a route on purpose — both create the job on the
2117
+ * slug — but a *client* library distinguishes them: `services.call` waits
2118
+ * for a terminal state, `actions.invoke` does not. Without the kind coming
2119
+ * back, calling a service helper on an action slug **starts the real action
2120
+ * on the robot** and then blames the robot for not finishing, half a minute
2121
+ * later. Returning the kind lets a client refuse its own mistake at once,
2122
+ * instead of reporting it as the machine's.
2123
+ */
2124
+ kind: import_zod11.z.enum(["datapoint", "action", "service", "publisher", "camera"]).nullable(),
2125
+ code: import_zod11.z.string().nullable(),
2126
+ message: import_zod11.z.string().nullable(),
2127
+ /**
2128
+ * The same payload the REST envelope carries in `apiError.details` — for
2129
+ * `parameter_invalid`, a `parameterInvalidDetails`.
2130
+ *
2131
+ * Added because it was missing, and its absence quietly broke §11.1: this
2132
+ * socket is supposed to do *everything* REST can do, but a
2133
+ * `parameter_invalid` arriving here had nowhere to put its violations, so
2134
+ * the same refusal was actionable over HTTP and opaque over the socket.
2135
+ * A client cannot bind an error to the input that caused it from a code
2136
+ * alone — which is the entire point of the flat parameter shape.
2137
+ */
2138
+ details: import_zod11.z.unknown().optional()
2139
+ });
2140
+ var errorFrame = import_zod11.z.object({
2141
+ type: import_zod11.z.literal("error"),
2142
+ code: import_zod11.z.string().min(1),
2143
+ message: import_zod11.z.string().min(1)
2144
+ });
2145
+ var clientSubscribe = import_zod11.z.object({
2146
+ type: import_zod11.z.literal("subscribe"),
2147
+ robot_id: import_zod11.z.uuid(),
2148
+ slug,
2149
+ /**
2150
+ * What the subscriber expects, and how it wants it (W5).
2151
+ *
2152
+ * `kind` lets the server answer **`wrong_kind`** instead of accepting a
2153
+ * subscribe the client will then filter to silence — and silence is
2154
+ * indistinguishable from an idle slug, so it tells a developer nothing.
2155
+ * Optional, so an older client that omits it keeps today's behaviour.
2156
+ *
2157
+ * `options` is where a camera says what it wants; a datapoint needs none.
2158
+ * It exists now rather than later because adding a field to a frame three
2159
+ * repos parse is cheap once and expensive twice.
2160
+ */
2161
+ /**
2162
+ * `publisher` is here even though a publisher is not subscribable: a client
2163
+ * that models the five grantable kinds and honestly names one gets the
2164
+ * informative `not_subscribable` the cloud already computes, instead of a
2165
+ * `validation_error` reciting an enum. Refusing the *word* rather than the
2166
+ * request was the same mistake as the silent wrong-verb subscribe this
2167
+ * field was added to fix.
2168
+ */
2169
+ kind: import_zod11.z.enum(["datapoint", "action", "service", "publisher", "camera"]).optional(),
2170
+ options: import_zod11.z.record(import_zod11.z.string(), import_zod11.z.unknown()).optional()
2171
+ });
2172
+ var clientUnsubscribe = import_zod11.z.object({
2173
+ type: import_zod11.z.literal("unsubscribe"),
2174
+ robot_id: import_zod11.z.uuid(),
2175
+ slug
2176
+ });
2177
+ var subscribeError = import_zod11.z.object({
2178
+ type: import_zod11.z.literal("subscribe_error"),
2179
+ robot_id: import_zod11.z.string(),
2180
+ slug: import_zod11.z.string(),
2181
+ code: import_zod11.z.string().min(1),
2182
+ message: import_zod11.z.string().min(1)
2183
+ });
2184
+ var datapointEvent = import_zod11.z.object({
2185
+ type: import_zod11.z.literal("datapoint"),
2186
+ robot_id: import_zod11.z.uuid(),
2187
+ slug,
2188
+ value: import_zod11.z.unknown(),
2189
+ timestamp_ms: import_zod11.z.number().int().nonnegative()
2190
+ });
2191
+ var resourceHealthEvent = import_zod11.z.object({
2192
+ type: import_zod11.z.literal("resource_health"),
2193
+ robot_id: import_zod11.z.uuid(),
2194
+ kind: import_zod11.z.enum(["camera", "credential"]),
2195
+ ref: import_zod11.z.string().min(1).max(64),
2196
+ state: import_zod11.z.enum(RESOURCE_HEALTH_STATES),
2197
+ reason: import_zod11.z.string().max(200).nullable(),
2198
+ changed_at_ms: import_zod11.z.number().int().nonnegative()
2199
+ });
2200
+
2201
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/identity.js
2202
+ var import_zod12 = require("zod");
2203
+ var password = import_zod12.z.string().min(12).max(256);
2204
+ var orgMemberRole = import_zod12.z.enum(["owner", "member"]);
2205
+ var org = import_zod12.z.object({
2206
+ id: import_zod12.z.uuid(),
2207
+ name: import_zod12.z.string().min(1).max(120),
2208
+ created_at: import_zod12.z.iso.datetime()
2209
+ });
2210
+ var orgMember = import_zod12.z.object({
2211
+ id: import_zod12.z.uuid(),
2212
+ org_id: import_zod12.z.uuid(),
2213
+ email: import_zod12.z.email(),
2214
+ role: orgMemberRole,
2215
+ created_at: import_zod12.z.iso.datetime()
2216
+ });
2217
+ var sessionTokens = import_zod12.z.object({
2218
+ access_token: import_zod12.z.string().min(1),
2219
+ refresh_token: import_zod12.z.string().min(1),
2220
+ expires_in: import_zod12.z.number().int().positive()
2221
+ });
2222
+ var refreshRequest = import_zod12.z.object({ refresh_token: import_zod12.z.string().min(1) });
2223
+ var signUpRequest = import_zod12.z.object({
2224
+ org_name: import_zod12.z.string().min(1).max(120),
2225
+ email: import_zod12.z.email(),
2226
+ password
2227
+ });
2228
+ var signUpResponse = import_zod12.z.object({
2229
+ org,
2230
+ member: orgMember,
2231
+ tokens: sessionTokens
2232
+ });
2233
+ var developerLoginRequest = import_zod12.z.object({
2234
+ email: import_zod12.z.email(),
2235
+ password: import_zod12.z.string().min(1)
2236
+ });
2237
+ var endUser = import_zod12.z.object({
2238
+ id: import_zod12.z.uuid(),
2239
+ org_id: import_zod12.z.uuid(),
2240
+ email: import_zod12.z.email(),
2241
+ status: import_zod12.z.enum(["invited", "active", "blocked"]),
2242
+ created_at: import_zod12.z.iso.datetime()
2243
+ });
2244
+ var mailStatus = import_zod12.z.enum(["sent", "not_configured", "failed"]);
2245
+ var invitation = import_zod12.z.object({
2246
+ id: import_zod12.z.uuid(),
2247
+ email: import_zod12.z.email(),
2248
+ app_id: import_zod12.z.uuid(),
2249
+ role_id: import_zod12.z.uuid(),
2250
+ expires_at: import_zod12.z.iso.datetime(),
2251
+ accept_url: import_zod12.z.url(),
2252
+ /** Replaces `mail_sent: boolean` — see `mailStatus` for why one bit was not enough. */
2253
+ mail: mailStatus
2254
+ });
2255
+ var createInvitationRequest = import_zod12.z.object({
2256
+ email: import_zod12.z.email(),
2257
+ app_id: import_zod12.z.uuid(),
2258
+ role_id: import_zod12.z.uuid(),
2259
+ send_mail: import_zod12.z.boolean()
2260
+ });
2261
+ var acceptInvitationRequest = import_zod12.z.object({
2262
+ token: import_zod12.z.string().min(1),
2263
+ password
2264
+ });
2265
+ var createDeveloperInvitationRequest = import_zod12.z.object({
2266
+ email: import_zod12.z.email(),
2267
+ role: orgMemberRole,
2268
+ send_mail: import_zod12.z.boolean()
2269
+ });
2270
+ var developerInvitation = import_zod12.z.object({
2271
+ id: import_zod12.z.uuid(),
2272
+ email: import_zod12.z.email(),
2273
+ role: orgMemberRole,
2274
+ expires_at: import_zod12.z.iso.datetime(),
2275
+ accept_url: import_zod12.z.url(),
2276
+ mail: mailStatus
2277
+ });
2278
+ var pendingDeveloperInvitation = import_zod12.z.object({
2279
+ id: import_zod12.z.uuid(),
2280
+ email: import_zod12.z.email(),
2281
+ role: orgMemberRole,
2282
+ expires_at: import_zod12.z.iso.datetime()
2283
+ });
2284
+ var developerInvitationListResponse = import_zod12.z.object({
2285
+ /** Pending only. An accepted invitation is history, not something to revoke. */
2286
+ invitations: import_zod12.z.array(pendingDeveloperInvitation)
2287
+ });
2288
+ var acceptDeveloperInvitationRequest = import_zod12.z.object({
2289
+ token: import_zod12.z.string().min(1),
2290
+ password
2291
+ });
2292
+ var tierRequiredDetails = import_zod12.z.object({
2293
+ required: orgMemberRole,
2294
+ /** The caller's own tier — theirs to know, and it is what makes the message actionable. */
2295
+ actual: orgMemberRole
2296
+ });
2297
+ var selfRegistration = import_zod12.z.object({
2298
+ enabled: import_zod12.z.boolean(),
2299
+ /**
2300
+ * Accept any address. Deliberately its own flag rather than a magic value
2301
+ * in `domains`, so "open to everyone" is something an app owner has to say,
2302
+ * not something that falls out of leaving a list empty.
2303
+ */
2304
+ all_domains: import_zod12.z.boolean(),
2305
+ /** Lower-case bare domains, no `@`: `['dehne-robotik.de']`. Ignored when `all_domains`. */
2306
+ domains: import_zod12.z.array(import_zod12.z.string().min(1).max(253)),
2307
+ /** The role every self-registered member of this pool receives (§3.2: exactly one per app). */
2308
+ role_id: import_zod12.z.uuid()
2309
+ });
2310
+ var clientRegisterRequest = import_zod12.z.object({
2311
+ app_identifier: appIdentifier,
2312
+ email: import_zod12.z.email(),
2313
+ password
2314
+ });
2315
+ var clientRegisterResponse = import_zod12.z.object({
2316
+ mail: mailStatus
2317
+ });
2318
+ var clientRegisterConfirm = import_zod12.z.object({
2319
+ token: import_zod12.z.string().min(1)
2320
+ });
2321
+ var passwordChangeRequest = import_zod12.z.object({
2322
+ current_password: import_zod12.z.string().min(1),
2323
+ new_password: password
2324
+ });
2325
+ var passwordResetRequest = import_zod12.z.object({
2326
+ email: import_zod12.z.email()
2327
+ });
2328
+ var clientPasswordResetRequest = import_zod12.z.object({
2329
+ app_identifier: appIdentifier,
2330
+ email: import_zod12.z.email()
2331
+ });
2332
+ var passwordResetConfirm = import_zod12.z.object({
2333
+ token: import_zod12.z.string().min(1),
2334
+ new_password: password
2335
+ });
2336
+ var idpClaimMapping = import_zod12.z.object({
2337
+ /** Which claim is the stable identity. `sub` unless the developer knows better. */
2338
+ subject_claim: import_zod12.z.string().min(1).max(100).default("sub"),
2339
+ email_claim: import_zod12.z.string().min(1).max(100).default("email")
2340
+ });
2341
+ var idpIssuer = import_zod12.z.url().max(500).refine((v) => {
2342
+ let url;
2343
+ try {
2344
+ url = new URL(v);
2345
+ } catch {
2346
+ return false;
2347
+ }
2348
+ if (!["http:", "https:"].includes(url.protocol))
2349
+ return false;
2350
+ if (url.username !== "" || url.password !== "")
2351
+ return false;
2352
+ if (url.hash !== "" || url.search !== "")
2353
+ return false;
2354
+ return url.hostname.length > 0;
2355
+ }, { message: "issuer must be an http(s) URL with no credentials, query or fragment" });
2356
+ var idpConfig = import_zod12.z.object({
2357
+ app_id: import_zod12.z.uuid(),
2358
+ issuer: idpIssuer,
2359
+ client_id: import_zod12.z.string().min(1).max(200),
2360
+ scopes: import_zod12.z.array(import_zod12.z.string().min(1).max(60)).min(1).max(20),
2361
+ claims: idpClaimMapping,
2362
+ link_verified_emails: import_zod12.z.boolean(),
2363
+ /** Never the secret itself — see `idpConfigRequest`. */
2364
+ has_client_secret: import_zod12.z.boolean(),
2365
+ updated_at: import_zod12.z.iso.datetime()
2366
+ });
2367
+ var idpConfigRequest = import_zod12.z.object({
2368
+ issuer: idpIssuer,
2369
+ client_id: import_zod12.z.string().min(1).max(200),
2370
+ client_secret: import_zod12.z.string().min(1).max(500).optional(),
2371
+ scopes: import_zod12.z.array(import_zod12.z.string().min(1).max(60)).min(1).max(20),
2372
+ claims: idpClaimMapping.optional(),
2373
+ link_verified_emails: import_zod12.z.boolean()
2374
+ }).strict();
2375
+
2376
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/audit.js
2377
+ var import_zod13 = require("zod");
2378
+ var auditActor = import_zod13.z.object({
2379
+ kind: import_zod13.z.enum(["developer", "end_user", "server_key", "bridge"]),
2380
+ id: import_zod13.z.uuid(),
2381
+ label: import_zod13.z.string().min(1).max(200)
2382
+ });
2383
+ var auditEvent = import_zod13.z.object({
2384
+ id: import_zod13.z.uuid(),
2385
+ org_id: import_zod13.z.uuid(),
2386
+ at: import_zod13.z.iso.datetime(),
2387
+ /**
2388
+ * A monotonic counter, ascending in write order, unique across the log
2389
+ * (W6b).
2390
+ *
2391
+ * `at` is not a total order. Two events written in the same millisecond —
2392
+ * a login and the config publish it enables, a cascade writing several
2393
+ * rows — sort against each other arbitrarily, and "arbitrarily" means
2394
+ * *differently on each query*. A reader paging through "newest first" can
2395
+ * therefore see one of them twice and the other not at all, which is the
2396
+ * one failure mode an audit log may not have: a record that is present and
2397
+ * invisible.
2398
+ *
2399
+ * It is also the only correct **cursor** for paging this log, for the same
2400
+ * reason: a cursor that is not unique either skips rows or repeats them at
2401
+ * every page boundary. No cursor parameter exists on `GET /api/audit` yet —
2402
+ * the route returns the whole log — and that is stated here rather than
2403
+ * implied, because a contract that describes a capability the API does not
2404
+ * have is the defect this project keeps finding. When paging is added it
2405
+ * uses this field; nothing else in this shape can carry it.
2406
+ *
2407
+ * Required, not optional: an event without a sequence cannot be ordered
2408
+ * against one that has it, and a log with two orderings has none.
2409
+ */
2410
+ seq: import_zod13.z.number().int().positive(),
2411
+ actor: auditActor,
2412
+ /** Stable dotted name, e.g. `end_user.invited`, `config.published`. */
2413
+ action: import_zod13.z.string().min(1).max(80),
2414
+ /**
2415
+ * What the action was about, if anything — a robot, an app, a user. Free
2416
+ * of ids the console cannot resolve: carry the label with it.
2417
+ */
2418
+ target: import_zod13.z.object({
2419
+ kind: import_zod13.z.string().min(1).max(40),
2420
+ id: import_zod13.z.string().min(1),
2421
+ label: import_zod13.z.string().min(1).max(200)
2422
+ }).nullable(),
2423
+ /** Action-specific extras. Never credentials, never tokens. */
2424
+ details: import_zod13.z.record(import_zod13.z.string(), import_zod13.z.unknown()).nullable()
2425
+ });
2426
+ var auditListResponse = import_zod13.z.object({
2427
+ events: import_zod13.z.array(auditEvent)
2428
+ });
2429
+
2430
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/errors.js
2431
+ var import_zod14 = require("zod");
2432
+ var apiError = import_zod14.z.object({
2433
+ code: import_zod14.z.string().min(1),
2434
+ message: import_zod14.z.string().min(1),
2435
+ details: import_zod14.z.unknown().optional()
2436
+ });
2437
+ var parameterViolation = import_zod14.z.object({
2438
+ field: import_zod14.z.string().min(1),
2439
+ /** Which rule failed — `min`, `max`, `enum`, `pattern`, `required`, `undeclared`. */
2440
+ rule: import_zod14.z.string().min(1),
2441
+ message: import_zod14.z.string().min(1)
2442
+ });
2443
+ var parameterInvalidDetails = import_zod14.z.object({
2444
+ violations: import_zod14.z.array(parameterViolation).min(1)
2445
+ });
2446
+
2447
+ // node_modules/.pnpm/@fleetless+contracts@git+ssh+++git@gitlab.dehne-robotik.de+fleetless+fleetless-contract_367ffc97d258be510904ed62d5d477a4/node_modules/@fleetless/contracts/dist/oauth.js
2448
+ var import_zod15 = require("zod");
2449
+ var oauthErrorCode = import_zod15.z.enum([
2450
+ "invalid_request",
2451
+ "invalid_client",
2452
+ "invalid_grant",
2453
+ "unauthorized_client",
2454
+ "unsupported_grant_type",
2455
+ "invalid_scope",
2456
+ "access_denied",
2457
+ "server_error",
2458
+ "temporarily_unavailable",
2459
+ /** RFC 8707: the `resource` named is not one this server issues tokens for. */
2460
+ "invalid_target"
2461
+ ]);
2462
+ var oauthError = import_zod15.z.object({
2463
+ error: oauthErrorCode,
2464
+ error_description: import_zod15.z.string().min(1).max(500).optional(),
2465
+ /** Echoed back per RFC 6749 §4.1.2.1 so a client can match the response. */
2466
+ state: import_zod15.z.string().min(1).max(500).optional(),
2467
+ /**
2468
+ * **A Fleetless reason carried inside a standard envelope, and it exists
2469
+ * because the alternative lost a distinction.**
2470
+ *
2471
+ * Two policy refusals at `/oauth/register` — the app has not opted in, and
2472
+ * the app's client ceiling is full — both map to RFC 6749's `access_denied`,
2473
+ * which is the honest standard code for either. Answering with only that
2474
+ * makes the two indistinguishable to the caller, and *a field that cannot
2475
+ * express a distinction produces a workaround somewhere else*. Answering in
2476
+ * `apiError` instead would keep the distinction and hand an RFC-compliant
2477
+ * client a body it cannot parse — which is the conformance this wave exists
2478
+ * to provide.
2479
+ *
2480
+ * So both: `error` is what a standard client reads, `fleetless_code` is what
2481
+ * our own tooling switches on. RFC 6749 §5.2 permits additional members, and
2482
+ * a client that ignores this one still behaves correctly.
2483
+ */
2484
+ fleetless_code: import_zod15.z.string().min(1).max(60).optional()
2485
+ });
2486
+ var oauthClientRegistration = import_zod15.z.enum(["developer", "dynamic"]);
2487
+ var redirectUri = import_zod15.z.string().min(1).max(2e3).refine((v) => {
2488
+ let url;
2489
+ try {
2490
+ url = new URL(v);
2491
+ } catch {
2492
+ return false;
2493
+ }
2494
+ if (url.hash !== "")
2495
+ return false;
2496
+ if (url.protocol === "https:")
2497
+ return url.hostname.length > 0;
2498
+ if (url.protocol === "http:")
2499
+ return ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
2500
+ return false;
2501
+ }, { message: "redirect_uri must be an https URL, or http on an explicit loopback address, and carry no fragment" });
2502
+ var codeChallengeMethod = import_zod15.z.enum(["S256"]);
2503
+ var oauthClient = import_zod15.z.object({
2504
+ id: import_zod15.z.uuid(),
2505
+ app_id: import_zod15.z.uuid(),
2506
+ /** The `client_id` on the wire — opaque, and not the app identifier. */
2507
+ client_id: import_zod15.z.string().min(1).max(200),
2508
+ client_name: import_zod15.z.string().min(1).max(200),
2509
+ registration: oauthClientRegistration,
2510
+ redirect_uris: import_zod15.z.array(redirectUri).min(1).max(20),
2511
+ created_at: import_zod15.z.iso.datetime(),
2512
+ /**
2513
+ * **Only `dynamic` clients expire, and the field is nullable rather than
2514
+ * absent so a consumer must decide what it means.** A self-registered client
2515
+ * that never completes a flow is an unauthenticated write somebody left
2516
+ * behind; a developer's own client is a configured thing that should not
2517
+ * vanish under them.
2518
+ */
2519
+ expires_at: import_zod15.z.iso.datetime().nullable(),
2520
+ last_used_at: import_zod15.z.iso.datetime().nullable()
2521
+ });
2522
+ var dynamicClientRegistrationRequest = import_zod15.z.object({
2523
+ client_name: import_zod15.z.string().min(1).max(200),
2524
+ redirect_uris: import_zod15.z.array(redirectUri).min(1).max(20),
2525
+ /** Accepted and echoed for conformance; this server issues only this pair. */
2526
+ grant_types: import_zod15.z.array(import_zod15.z.enum(["authorization_code", "refresh_token"])).optional(),
2527
+ response_types: import_zod15.z.array(import_zod15.z.enum(["code"])).optional(),
2528
+ /** RFC 7591 allows `none` for public clients; OAuth 2.1 + PKCE is the defence. */
2529
+ token_endpoint_auth_method: import_zod15.z.enum(["none"]).optional(),
2530
+ scope: import_zod15.z.string().max(500).optional()
2531
+ }).strict();
2532
+ var dynamicClientRegistrationResponse = import_zod15.z.object({
2533
+ client_id: import_zod15.z.string().min(1).max(200),
2534
+ client_name: import_zod15.z.string().min(1).max(200),
2535
+ redirect_uris: import_zod15.z.array(redirectUri),
2536
+ grant_types: import_zod15.z.array(import_zod15.z.string()),
2537
+ response_types: import_zod15.z.array(import_zod15.z.string()),
2538
+ token_endpoint_auth_method: import_zod15.z.literal("none"),
2539
+ client_id_issued_at: import_zod15.z.number().int().nonnegative(),
2540
+ /** Seconds since the epoch, per RFC 7591. `0` would mean "never expires". */
2541
+ client_secret_expires_at: import_zod15.z.literal(0)
2542
+ });
2543
+ var oauthTokenRequest = import_zod15.z.discriminatedUnion("grant_type", [
2544
+ import_zod15.z.object({
2545
+ grant_type: import_zod15.z.literal("authorization_code"),
2546
+ code: import_zod15.z.string().min(1).max(500),
2547
+ redirect_uri: redirectUri,
2548
+ client_id: import_zod15.z.string().min(1).max(200),
2549
+ code_verifier: import_zod15.z.string().regex(/^[A-Za-z0-9\-._~]{43,128}$/, "code_verifier must be 43-128 unreserved characters (RFC 7636 \xA74.1)"),
2550
+ resource: import_zod15.z.url().optional()
2551
+ }),
2552
+ import_zod15.z.object({
2553
+ grant_type: import_zod15.z.literal("refresh_token"),
2554
+ refresh_token: import_zod15.z.string().min(1).max(500),
2555
+ client_id: import_zod15.z.string().min(1).max(200),
2556
+ resource: import_zod15.z.url().optional(),
2557
+ /** RFC 6749 §6 — a refresh may narrow scope, never widen it. */
2558
+ scope: import_zod15.z.string().max(500).optional()
2559
+ })
2560
+ ]);
2561
+ var oauthTokenResponse = import_zod15.z.object({
2562
+ access_token: import_zod15.z.string().min(1),
2563
+ token_type: import_zod15.z.literal("Bearer"),
2564
+ /** Seconds, per RFC 6749 §5.1 — not a timestamp, and not milliseconds. */
2565
+ expires_in: import_zod15.z.number().int().positive(),
2566
+ refresh_token: import_zod15.z.string().min(1).optional(),
2567
+ scope: import_zod15.z.string().max(500).optional()
2568
+ });
2569
+ var authorizationServerMetadata = import_zod15.z.object({
2570
+ issuer: import_zod15.z.url(),
2571
+ authorization_endpoint: import_zod15.z.url(),
2572
+ token_endpoint: import_zod15.z.url(),
2573
+ registration_endpoint: import_zod15.z.url().optional(),
2574
+ response_types_supported: import_zod15.z.array(import_zod15.z.literal("code")),
2575
+ grant_types_supported: import_zod15.z.array(import_zod15.z.enum(["authorization_code", "refresh_token"])),
2576
+ code_challenge_methods_supported: import_zod15.z.array(codeChallengeMethod),
2577
+ token_endpoint_auth_methods_supported: import_zod15.z.array(import_zod15.z.literal("none")),
2578
+ scopes_supported: import_zod15.z.array(import_zod15.z.string()).optional()
2579
+ });
2580
+ var protectedResourceMetadata = import_zod15.z.object({
2581
+ resource: import_zod15.z.url(),
2582
+ authorization_servers: import_zod15.z.array(import_zod15.z.url()).min(1),
2583
+ bearer_methods_supported: import_zod15.z.array(import_zod15.z.literal("header")),
2584
+ scopes_supported: import_zod15.z.array(import_zod15.z.string()).optional()
2585
+ });
2586
+ var consentGrant = import_zod15.z.object({
2587
+ client_id: import_zod15.z.string().min(1).max(200),
2588
+ app_id: import_zod15.z.uuid(),
2589
+ end_user_id: import_zod15.z.uuid(),
2590
+ role_id: import_zod15.z.uuid(),
2591
+ scope: import_zod15.z.string().max(500),
2592
+ granted_at: import_zod15.z.iso.datetime()
2593
+ });
2594
+ var oauthInteraction = import_zod15.z.object({
2595
+ interaction_id: import_zod15.z.string().min(1).max(200),
2596
+ app_name: import_zod15.z.string().min(1).max(120),
2597
+ idp: import_zod15.z.object({ button_label: import_zod15.z.string().min(1).max(60) }).nullable()
2598
+ });
2599
+ var oauthConsentInteraction = import_zod15.z.object({
2600
+ interaction_id: import_zod15.z.string().min(1).max(200),
2601
+ app_name: import_zod15.z.string().min(1).max(120),
2602
+ client_name: import_zod15.z.string().min(1).max(200),
2603
+ registration: oauthClientRegistration,
2604
+ role_name: import_zod15.z.string().min(1).max(120),
2605
+ scope: import_zod15.z.string().max(500)
2606
+ });
2607
+ var oauthLoginRequest = import_zod15.z.object({
2608
+ interaction_id: import_zod15.z.string().min(1).max(200),
2609
+ email: import_zod15.z.email(),
2610
+ password: import_zod15.z.string().min(1)
2611
+ }).strict();
2612
+ var oauthRedirectResponse = import_zod15.z.object({
2613
+ redirect_to: import_zod15.z.string().min(1).max(2e3)
2614
+ });
2615
+ var consentDecision = import_zod15.z.object({
2616
+ /** The opaque handle the authorize step handed the consent screen. */
2617
+ interaction_id: import_zod15.z.string().min(1).max(200),
2618
+ approved: import_zod15.z.boolean()
2619
+ });
2620
+ var OAUTH_PATHS = {
2621
+ authorizationServerMetadata: "/.well-known/oauth-authorization-server",
2622
+ protectedResourceMetadata: "/.well-known/oauth-protected-resource",
2623
+ authorize: "/oauth/authorize",
2624
+ token: "/oauth/token",
2625
+ register: "/oauth/register",
2626
+ /**
2627
+ * **One path, both verbs** — `GET` serves the consent page, `POST` accepts a
2628
+ * `consentDecision`. Decided 2026-08-18 rather than left to be inferred:
2629
+ * Eve-W7b asked whether the page's own GET route was this path or another,
2630
+ * which is the right question and had no answer anywhere.
2631
+ *
2632
+ * One entry means the console and the cloud cannot drift apart on it, which
2633
+ * is the failure W7a paid for when five hand-written copies of `assetKind`
2634
+ * and a header name crossed the TypeScript/Python line. Note that the two
2635
+ * verbs answer differently: the page is HTML, and a failed `POST` answers
2636
+ * `apiError` — see the dialect note at the top of this file, which names
2637
+ * this path as the exception the prefix will mislead you about.
2638
+ */
2639
+ consent: "/oauth/consent",
2640
+ /**
2641
+ * The two federation legs. **Server-owned redirect targets a client never
2642
+ * constructs** — the same category as `authorize`, `token` and `register`,
2643
+ * and the reason they belong here rather than as literals.
2644
+ *
2645
+ * Kassandra-W7b found `/oauth/idp-start` written as a literal in
2646
+ * `cloud/src/routes/oauth-federation.ts` **and** in
2647
+ * `console/oauth-pages/login/src/App.vue` — two repos agreeing on a string
2648
+ * with nothing shared between them. Worse than the `/idp` versus
2649
+ * `/idp-config` mismatch this wave already met, because the console half
2650
+ * ships as a **committed artifact**: the drift would survive a re-pin and an
2651
+ * install, and the symptom is a federation button that navigates to a 404,
2652
+ * invisible to both suites.
2653
+ *
2654
+ * `OAUTH_PATHS` was created in this wave with a doc comment citing W7a's
2655
+ * five hand-written copies of `assetKind`. The rule was applied to `login`
2656
+ * and `consent` and stopped there.
2657
+ */
2658
+ idpStart: "/oauth/idp-start",
2659
+ idpCallback: "/oauth/idp-callback",
2660
+ /**
2661
+ * The console-built page the cloud serves from its own origin (see §3.4) —
2662
+ * and, like `consent` above, **one path with both verbs**: `GET` serves the
2663
+ * page, `POST` accepts an `oauthLoginRequest` and answers an
2664
+ * `oauthLoginResponse`.
2665
+ *
2666
+ * Decided 2026-08-18. Nimbus-W7b proposed a separate `POST /oauth/login`,
2667
+ * which would work; one path is chosen for the same reason `consent` has
2668
+ * one — the page posts to its own URL, so there is no second string for two
2669
+ * repos to disagree about, and this wave has already produced one live
2670
+ * mismatch of exactly that kind (`/idp` versus `/idp-config`, caught by
2671
+ * comparing repos rather than by either suite).
2672
+ *
2673
+ * A failed `POST` answers `apiError`, not `oauthError`: the caller is our own
2674
+ * page, not a standard client. See the dialect note at the top of this file.
2675
+ */
2676
+ login: "/login"
2677
+ };
2678
+
2679
+ // src/pkce.ts
2680
+ function requireCrypto() {
2681
+ const c = globalThis.crypto;
2682
+ if (!c?.subtle) {
2683
+ throw new Error(
2684
+ "auth.beginHostedLogin/completeHostedLogin need the Web Crypto API (globalThis.crypto.subtle), which is not available in this environment."
2685
+ );
2686
+ }
2687
+ return c;
2688
+ }
2689
+ function toBase64Url(bytes) {
2690
+ let binary = "";
2691
+ for (const byte of bytes) binary += String.fromCharCode(byte);
2692
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2693
+ }
2694
+ function randomBase64Url(byteLength) {
2695
+ const bytes = new Uint8Array(byteLength);
2696
+ requireCrypto().getRandomValues(bytes);
2697
+ return toBase64Url(bytes);
2698
+ }
2699
+ function generateCodeVerifier() {
2700
+ return randomBase64Url(32);
2701
+ }
2702
+ function generateState() {
2703
+ return randomBase64Url(32);
2704
+ }
2705
+ async function computeCodeChallenge(codeVerifier) {
2706
+ const digest = await requireCrypto().subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
2707
+ return toBase64Url(new Uint8Array(digest));
2708
+ }
2709
+
2710
+ // src/auth.ts
2711
+ var SessionCredentials = class {
2712
+ constructor(http, tokenStore) {
2713
+ this.http = http;
2714
+ this.tokenStore = tokenStore;
2715
+ }
2716
+ http;
2717
+ tokenStore;
2718
+ #refreshing = null;
2719
+ async token() {
2720
+ const session = await this.tokenStore.load();
2721
+ return session ? session.access_token : null;
2722
+ }
2723
+ async handleExpired() {
2724
+ await this.#ensureRefreshed();
2725
+ return true;
2726
+ }
2727
+ /** Single-flight — a refresh already in progress is awaited, never duplicated. */
2728
+ #ensureRefreshed() {
2729
+ if (this.#refreshing) return this.#refreshing;
2730
+ const attempt = this.#refresh().finally(() => {
2731
+ this.#refreshing = null;
2732
+ });
2733
+ this.#refreshing = attempt;
2734
+ return attempt;
2735
+ }
2736
+ async #refresh() {
2737
+ const current = await this.tokenStore.load();
2738
+ if (!current) throw new FleetlessError("no_session", "No session to refresh.");
2739
+ const body = { refresh_token: current.refresh_token };
2740
+ const tokens = await this.http.request("/api/client/refresh", {
2741
+ method: "POST",
2742
+ skipAuth: true,
2743
+ body
2744
+ });
2745
+ await this.tokenStore.save(tokens);
2746
+ return tokens;
2747
+ }
2748
+ };
2749
+ var ServerKeyCredentials = class {
2750
+ constructor(serverKey2) {
2751
+ this.serverKey = serverKey2;
2752
+ }
2753
+ serverKey;
2754
+ async token() {
2755
+ return this.serverKey;
2756
+ }
2757
+ async handleExpired() {
2758
+ return false;
2759
+ }
2760
+ };
2761
+ function createPasswordResetMethods(http, appIdentifier2) {
2762
+ return {
2763
+ async requestPasswordReset(email) {
2764
+ const body = { app_identifier: appIdentifier2, email };
2765
+ await http.request("/api/client/password/reset", { method: "POST", skipAuth: true, body, expectEmptyBody: true });
2766
+ },
2767
+ async confirmPasswordReset(token, newPassword) {
2768
+ const body = { token, new_password: newPassword };
2769
+ await http.request("/api/client/password/reset/confirm", { method: "POST", skipAuth: true, body, expectEmptyBody: true });
2770
+ }
2771
+ };
2772
+ }
2773
+ function createSessionAuth(http, tokenStore, appIdentifier2) {
2774
+ return {
2775
+ async login(email, password2) {
2776
+ const body = { app_identifier: appIdentifier2, email, password: password2 };
2777
+ const tokens = await http.request("/api/client/login", { method: "POST", skipAuth: true, body });
2778
+ await tokenStore.save(tokens);
2779
+ },
2780
+ async beginHostedLogin(options) {
2781
+ const state = generateState();
2782
+ const codeVerifier = generateCodeVerifier();
2783
+ const codeChallenge = await computeCodeChallenge(codeVerifier);
2784
+ const url = new URL(`${http.baseUrl}${OAUTH_PATHS.authorize}`);
2785
+ url.searchParams.set("response_type", "code");
2786
+ url.searchParams.set("client_id", options.clientId);
2787
+ url.searchParams.set("redirect_uri", options.redirectUri);
2788
+ url.searchParams.set("code_challenge", codeChallenge);
2789
+ url.searchParams.set("code_challenge_method", "S256");
2790
+ url.searchParams.set("state", state);
2791
+ if (options.scope !== void 0) url.searchParams.set("scope", options.scope);
2792
+ if (options.resource !== void 0) url.searchParams.set("resource", options.resource);
2793
+ return { url: url.toString(), state, codeVerifier };
2794
+ },
2795
+ async completeHostedLogin(options) {
2796
+ if (!options.expectedState) {
2797
+ throw new FleetlessError(
2798
+ "no_hosted_login_attempt",
2799
+ "completeHostedLogin: expectedState is empty \u2014 nothing was persisted for this attempt. This usually means the callback landed in a different tab or window than the one that called beginHostedLogin, the session was restored, or storage was cleared in between \u2014 not necessarily an attack. Check what beginHostedLogin returned and how your app persisted it."
2800
+ );
2801
+ }
2802
+ if (!options.state || options.state !== options.expectedState) {
2803
+ throw new FleetlessError(
2804
+ "state_mismatch",
2805
+ "completeHostedLogin: the redirect's state does not match the state beginHostedLogin generated for this attempt \u2014 refusing to complete a login this client did not start (RFC 6749 \xA710.12)."
2806
+ );
2807
+ }
2808
+ const raw = await http.requestOAuth(OAUTH_PATHS.token, {
2809
+ grant_type: "authorization_code",
2810
+ code: options.code,
2811
+ redirect_uri: options.redirectUri,
2812
+ client_id: options.clientId,
2813
+ code_verifier: options.codeVerifier,
2814
+ ...options.resource !== void 0 ? { resource: options.resource } : {}
2815
+ });
2816
+ if (raw.refresh_token === void 0) {
2817
+ throw new FleetlessError(
2818
+ "unexpected_response",
2819
+ "POST /oauth/token answered ok but did not include a refresh_token; a hosted-login session cannot silently refresh without one."
2820
+ );
2821
+ }
2822
+ const tokens = { access_token: raw.access_token, refresh_token: raw.refresh_token, expires_in: raw.expires_in };
2823
+ await tokenStore.save(tokens);
2824
+ },
2825
+ async logout() {
2826
+ const session = await tokenStore.load();
2827
+ let revoked = true;
2828
+ if (session) {
2829
+ const body = { refresh_token: session.refresh_token };
2830
+ revoked = await http.request("/api/client/logout", { method: "POST", skipAuth: true, body, expectEmptyBody: true }).then(() => true).catch(() => false);
2831
+ }
2832
+ await tokenStore.save(null);
2833
+ return { revoked };
2834
+ },
2835
+ async me() {
2836
+ return http.request("/api/client/me", {});
2837
+ },
2838
+ async register(email, password2) {
2839
+ const body = { app_identifier: appIdentifier2, email, password: password2 };
2840
+ return http.request("/api/client/register", { method: "POST", skipAuth: true, body });
2841
+ },
2842
+ async confirmRegistration(token) {
2843
+ const body = { token };
2844
+ const tokens = await http.request("/api/client/register/confirm", { method: "POST", skipAuth: true, body });
2845
+ await tokenStore.save(tokens);
2846
+ },
2847
+ async changePassword(currentPassword, newPassword) {
2848
+ const body = { current_password: currentPassword, new_password: newPassword };
2849
+ const tokens = await http.request("/api/client/password/change", { method: "POST", body });
2850
+ await tokenStore.save(tokens);
2851
+ },
2852
+ ...createPasswordResetMethods(http, appIdentifier2)
2853
+ };
2854
+ }
2855
+ function createServerKeyAuth(http, appIdentifier2) {
2856
+ return {
2857
+ async login() {
2858
+ throw new Error("auth.login is not available on a client constructed with a serverKey.");
2859
+ },
2860
+ async beginHostedLogin() {
2861
+ throw new Error("auth.beginHostedLogin is not available on a client constructed with a serverKey.");
2862
+ },
2863
+ async completeHostedLogin() {
2864
+ throw new Error("auth.completeHostedLogin is not available on a client constructed with a serverKey.");
2865
+ },
2866
+ async logout() {
2867
+ throw new Error("auth.logout is not available on a client constructed with a serverKey.");
2868
+ },
2869
+ async me() {
2870
+ return http.request("/api/client/me", {});
2871
+ },
2872
+ async register() {
2873
+ throw new Error("auth.register is not available on a client constructed with a serverKey.");
2874
+ },
2875
+ async confirmRegistration() {
2876
+ throw new Error("auth.confirmRegistration is not available on a client constructed with a serverKey.");
2877
+ },
2878
+ async changePassword() {
2879
+ throw new Error("auth.changePassword is not available on a client constructed with a serverKey.");
2880
+ },
2881
+ ...createPasswordResetMethods(http, appIdentifier2)
2882
+ };
2883
+ }
2884
+
2885
+ // src/cameras.ts
2886
+ var EMPTY_SNAPSHOT_META = { mime: null, width: null, height: null, timestamp_ms: null, age_ms: null };
2887
+ function headerNumber(headers, name) {
2888
+ const raw = headers.get(name);
2889
+ if (raw === null) return null;
2890
+ const value = Number(raw);
2891
+ return Number.isFinite(value) ? value : null;
2892
+ }
2893
+ function createCamerasApi(http) {
2894
+ return {
2895
+ async list(robotId) {
2896
+ const response = await http.request(`/api/robots/${pathSegment(robotId)}/cameras`, {});
2897
+ return response.cameras;
2898
+ },
2899
+ async snapshot(robotId, slug2) {
2900
+ try {
2901
+ const { body, headers } = await http.requestBinary(`/api/robots/${pathSegment(robotId)}/cameras/${pathSegment(slug2)}/snapshot`);
2902
+ return {
2903
+ image: body,
2904
+ mime: mimeFromContentType(headers),
2905
+ width: headerNumber(headers, SNAPSHOT_HEADERS.width),
2906
+ height: headerNumber(headers, SNAPSHOT_HEADERS.height),
2907
+ timestamp_ms: headerNumber(headers, SNAPSHOT_HEADERS.timestampMs),
2908
+ age_ms: headerNumber(headers, SNAPSHOT_HEADERS.ageMs)
2909
+ };
2910
+ } catch (error) {
2911
+ if (error instanceof FleetlessError && error.code === "no_snapshot_yet") return { image: null, ...EMPTY_SNAPSHOT_META };
2912
+ throw error;
2913
+ }
2914
+ },
2915
+ async snapshotMeta(robotId, slug2) {
2916
+ try {
2917
+ const response = await http.request(`/api/robots/${pathSegment(robotId)}/cameras/${pathSegment(slug2)}/snapshot/meta`, {});
2918
+ return {
2919
+ mime: response.mime,
2920
+ width: response.width,
2921
+ height: response.height,
2922
+ timestamp_ms: response.timestamp_ms,
2923
+ age_ms: response.age_ms
2924
+ };
2925
+ } catch (error) {
2926
+ if (error instanceof FleetlessError && error.code === "no_snapshot_yet") return { ...EMPTY_SNAPSHOT_META };
2927
+ throw error;
2928
+ }
2929
+ },
2930
+ async live(robotId, slug2) {
2931
+ const response = await http.request(`/api/robots/${pathSegment(robotId)}/cameras/${pathSegment(slug2)}/live`, {
2932
+ method: "POST"
2933
+ });
2934
+ const releasePath = `/api/robots/${pathSegment(robotId)}/cameras/${pathSegment(slug2)}/live?${new URLSearchParams({ session_id: response.session_id })}`;
2935
+ let releasePromise = null;
2936
+ const release = () => {
2937
+ if (!releasePromise) {
2938
+ releasePromise = http.request(releasePath, { method: "DELETE", expectEmptyBody: true }).then(() => void 0).catch(() => void 0);
2939
+ }
2940
+ return releasePromise;
2941
+ };
2942
+ return {
2943
+ session_id: response.session_id,
2944
+ url: response.url,
2945
+ room: response.room,
2946
+ token: response.token,
2947
+ expires_at: response.expires_at,
2948
+ release
2949
+ };
2950
+ }
2951
+ };
2952
+ }
2953
+
2954
+ // src/commands.ts
2955
+ function generateRequestId() {
2956
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
2957
+ return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
2958
+ }
2959
+ var DEFAULT_COMMAND_TIMEOUT_MS = 1e4;
2960
+ var LOCAL_WAIT_MARGIN_MS = 5e3;
2961
+ function resolveLocalWaitMs(options, defaultMs) {
2962
+ const { timeoutMs, patienceMs } = options;
2963
+ if (timeoutMs !== void 0 && patienceMs !== void 0 && timeoutMs < patienceMs) {
2964
+ throw new FleetlessError(
2965
+ "invalid_option",
2966
+ `timeoutMs (${timeoutMs}ms) is shorter than patienceMs (${patienceMs}ms) \u2014 the SDK would give up locally before the platform's own patience runs out, and report command_timeout for a call the platform never actually refused. Raise timeoutMs above patienceMs, or omit timeoutMs to have it derived automatically.`
2967
+ );
2968
+ }
2969
+ if (timeoutMs !== void 0) return timeoutMs;
2970
+ if (patienceMs !== void 0) return patienceMs + LOCAL_WAIT_MARGIN_MS;
2971
+ return defaultMs;
2972
+ }
2973
+ function sendCommand(channel, frame, options = {}) {
2974
+ channel.connect();
2975
+ const timeoutMs = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
2976
+ return new Promise((resolve, reject) => {
2977
+ let settled = false;
2978
+ const cleanups = [];
2979
+ const cleanup = () => {
2980
+ for (const fn of cleanups) fn();
2981
+ };
2982
+ const settle = (fn) => {
2983
+ if (settled) return;
2984
+ settled = true;
2985
+ cleanup();
2986
+ fn();
2987
+ };
2988
+ const timer = setTimeout(() => {
2989
+ settle(
2990
+ () => reject(
2991
+ new FleetlessError(
2992
+ "command_timeout",
2993
+ `Command '${frame.type}' (request_id=${frame.request_id}) timed out after ${timeoutMs}ms without a reply.`
2994
+ )
2995
+ )
2996
+ );
2997
+ }, timeoutMs);
2998
+ cleanups.push(() => clearTimeout(timer));
2999
+ cleanups.push(
3000
+ channel.on("command_result", (raw) => {
3001
+ const result = raw;
3002
+ if (result.request_id !== frame.request_id) return;
3003
+ settle(() => {
3004
+ if (result.ok) {
3005
+ resolve(result);
3006
+ return;
3007
+ }
3008
+ const code = result.code ?? "command_failed";
3009
+ const details = code === "busy" && result.job ? { running: result.job } : result.details;
3010
+ reject(new FleetlessError(code, result.message ?? "The command was refused.", details !== void 0 ? { details } : void 0));
3011
+ });
3012
+ })
3013
+ );
3014
+ cleanups.push(
3015
+ channel.onAuthFailed((error) => {
3016
+ settle(() => reject(error));
3017
+ })
3018
+ );
3019
+ const watchForDeadConnection = (epochAtSend) => {
3020
+ cleanups.push(
3021
+ channel.onReady(() => {
3022
+ if (channel.connectionEpoch === epochAtSend) return;
3023
+ settle(
3024
+ () => reject(
3025
+ new FleetlessError(
3026
+ "command_outcome_unknown",
3027
+ `The realtime connection was re-established before a reply to '${frame.type}' (request_id=${frame.request_id}) arrived. The command may or may not have run \u2014 read the job by slug (e.g. actions.subscribe) rather than retrying blindly.`
3028
+ )
3029
+ )
3030
+ );
3031
+ })
3032
+ );
3033
+ };
3034
+ if (channel.isReady) {
3035
+ const epoch = channel.connectionEpoch;
3036
+ channel.send(frame);
3037
+ watchForDeadConnection(epoch);
3038
+ } else {
3039
+ const unlistenReady = channel.onReady(() => {
3040
+ unlistenReady();
3041
+ const epoch = channel.connectionEpoch;
3042
+ channel.send(frame);
3043
+ watchForDeadConnection(epoch);
3044
+ });
3045
+ cleanups.push(unlistenReady);
3046
+ }
3047
+ });
3048
+ }
3049
+ function assertValidJobId(jobId) {
3050
+ if (jobId === void 0 || jobId === null || typeof jobId === "string") return;
3051
+ throw new FleetlessError(
3052
+ "invalid_option",
3053
+ `cancel()'s third argument must be a job id (string), null, or omitted \u2014 got ${typeof jobId === "object" ? "an object" : typeof jobId}. If you are passing an options object (e.g. {timeoutMs}) as the third argument, note the signature changed in this release: cancel(robotId, slug) is unchanged, but a third positional argument is now the job id to cancel and options moved to a fourth argument \u2014 cancel(robotId, slug, jobId, options). See the README's Actions section.`
3054
+ );
3055
+ }
3056
+ function createRealtimeCommandTransport(channel) {
3057
+ return {
3058
+ // Declared `async` deliberately, unlike `cancel`/`publish` below: it is
3059
+ // the only one of the three that can refuse *before* sending anything
3060
+ // (resolveLocalWaitMs's `invalid_option`, D3a), and every caller of
3061
+ // this interface — starting with this file's own `sendCommand` callers
3062
+ // — is entitled to assume `CommandTransport.invoke` always returns a
3063
+ // promise rather than throwing synchronously. Without `async` here, a
3064
+ // synchronous throw from `resolveLocalWaitMs` would escape as a thrown
3065
+ // exception instead of a rejection, breaking that assumption for any
3066
+ // caller that isn't itself inside an `async` function (e.g. a test
3067
+ // calling this transport directly).
3068
+ async invoke(robotId, slug2, params, options) {
3069
+ const timeoutMs = resolveLocalWaitMs(options ?? {}, DEFAULT_COMMAND_TIMEOUT_MS);
3070
+ const frame = {
3071
+ type: "invoke",
3072
+ request_id: generateRequestId(),
3073
+ robot_id: robotId,
3074
+ slug: slug2,
3075
+ params,
3076
+ patience_ms: options?.patienceMs
3077
+ };
3078
+ return sendCommand(channel, frame, { timeoutMs });
3079
+ },
3080
+ // Declared `async` for the same reason `invoke` above is (D3a, D6):
3081
+ // `assertValidJobId` can throw before a frame is ever built, and a
3082
+ // caller of this interface is entitled to a rejected promise, never a
3083
+ // thrown exception.
3084
+ async cancel(robotId, slug2, jobId, options) {
3085
+ assertValidJobId(jobId);
3086
+ const frame = { type: "cancel", request_id: generateRequestId(), robot_id: robotId, slug: slug2, job_id: jobId ?? null };
3087
+ return sendCommand(channel, frame, options);
3088
+ },
3089
+ publish(robotId, slug2, message, options) {
3090
+ const frame = { type: "publish", request_id: generateRequestId(), robot_id: robotId, slug: slug2, message };
3091
+ return sendCommand(channel, frame, options);
3092
+ }
3093
+ };
3094
+ }
3095
+
3096
+ // src/datapoints.ts
3097
+ function keyOf(robotId, slug2) {
3098
+ return `${robotId} ${slug2}`;
3099
+ }
3100
+ function createKeyState(channel, slugSubscriptions, robotId, slug2) {
3101
+ const handlers = /* @__PURE__ */ new Set();
3102
+ const unlistenEvent = channel.on("datapoint", (frame) => {
3103
+ const event = frame;
3104
+ if (event.robot_id !== robotId || event.slug !== slug2) return;
3105
+ for (const handler of handlers) handler.onEvent(event);
3106
+ });
3107
+ const slugHandle = slugSubscriptions.acquire(robotId, slug2, {
3108
+ onSubscribeError(error) {
3109
+ for (const handler of handlers) handler.onError?.(error);
3110
+ },
3111
+ onAuthFailed(error) {
3112
+ for (const handler of handlers) handler.onError?.(error);
3113
+ }
3114
+ });
3115
+ return { handlers, unlistenEvent, slugHandle };
3116
+ }
3117
+ function historyQueryString(options) {
3118
+ const params = new URLSearchParams();
3119
+ params.set("from", options.from);
3120
+ if (options.to !== void 0) params.set("to", options.to);
3121
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
3122
+ if (options.aggregate) {
3123
+ params.set("window", options.aggregate.window);
3124
+ params.set("agg", options.aggregate.agg);
3125
+ if (options.aggregate.field !== void 0) params.set("field", options.aggregate.field);
3126
+ }
3127
+ return params.toString();
3128
+ }
3129
+ function createDatapointsApi(http, channel, slugSubscriptions) {
3130
+ const registry = /* @__PURE__ */ new Map();
3131
+ async function history(robotId, slug2, options) {
3132
+ const query = historyQueryString(options);
3133
+ const result = await http.request(
3134
+ `/api/robots/${pathSegment(robotId)}/datapoints/${pathSegment(slug2)}/history?${query}`,
3135
+ {}
3136
+ );
3137
+ return result;
3138
+ }
3139
+ return {
3140
+ async get(robotId, slug2) {
3141
+ const value = await http.request(`/api/robots/${pathSegment(robotId)}/datapoints/${pathSegment(slug2)}`, {});
3142
+ return value;
3143
+ },
3144
+ history,
3145
+ subscribe(robotId, slug2, handlers) {
3146
+ const key = keyOf(robotId, slug2);
3147
+ let state = registry.get(key);
3148
+ if (!state) {
3149
+ state = createKeyState(channel, slugSubscriptions, robotId, slug2);
3150
+ registry.set(key, state);
3151
+ }
3152
+ state.handlers.add(handlers);
3153
+ let active = true;
3154
+ return {
3155
+ unsubscribe() {
3156
+ if (!active) return;
3157
+ active = false;
3158
+ const current = registry.get(key);
3159
+ if (!current) return;
3160
+ current.handlers.delete(handlers);
3161
+ if (current.handlers.size > 0) return;
3162
+ current.unlistenEvent();
3163
+ current.slugHandle.release();
3164
+ registry.delete(key);
3165
+ }
3166
+ };
3167
+ }
3168
+ };
3169
+ }
3170
+
3171
+ // src/job-subscriptions.ts
3172
+ function keyOf2(robotId, slug2) {
3173
+ return `${robotId} ${slug2}`;
3174
+ }
3175
+ function createKeyState2(channel, slugSubscriptions, robotId, slug2) {
3176
+ const handlers = /* @__PURE__ */ new Set();
3177
+ const unlistenEvent = channel.on("job", (frame) => {
3178
+ const event = frame;
3179
+ if (event.robot_id !== robotId || event.slug !== slug2) return;
3180
+ for (const handler of handlers) handler.onJob(event);
3181
+ });
3182
+ const slugHandle = slugSubscriptions.acquire(robotId, slug2, {
3183
+ onSubscribeError(error) {
3184
+ for (const handler of handlers) handler.onError?.(error);
3185
+ },
3186
+ onAuthFailed(error) {
3187
+ for (const handler of handlers) handler.onError?.(error);
3188
+ }
3189
+ });
3190
+ return { handlers, unlistenEvent, slugHandle };
3191
+ }
3192
+ function createJobSubscriptions(channel, slugSubscriptions) {
3193
+ const registry = /* @__PURE__ */ new Map();
3194
+ return {
3195
+ subscribe(robotId, slug2, handlers) {
3196
+ const key = keyOf2(robotId, slug2);
3197
+ let state = registry.get(key);
3198
+ if (!state) {
3199
+ state = createKeyState2(channel, slugSubscriptions, robotId, slug2);
3200
+ registry.set(key, state);
3201
+ }
3202
+ state.handlers.add(handlers);
3203
+ let active = true;
3204
+ return {
3205
+ unsubscribe() {
3206
+ if (!active) return;
3207
+ active = false;
3208
+ const current = registry.get(key);
3209
+ if (!current) return;
3210
+ current.handlers.delete(handlers);
3211
+ if (current.handlers.size > 0) return;
3212
+ current.unlistenEvent();
3213
+ current.slugHandle.release();
3214
+ registry.delete(key);
3215
+ }
3216
+ };
3217
+ }
3218
+ };
3219
+ }
3220
+
3221
+ // src/jobs.ts
3222
+ function createJobsApi(http) {
3223
+ return {
3224
+ async list(robotId) {
3225
+ const response = await http.request(`/api/robots/${pathSegment(robotId)}/jobs`, {});
3226
+ return response.jobs;
3227
+ }
3228
+ };
3229
+ }
3230
+
3231
+ // src/publishers.ts
3232
+ function createPublishersApi(transport) {
3233
+ return {
3234
+ async publish(robotId, slug2, message, options) {
3235
+ await transport.publish(robotId, slug2, message, options);
3236
+ }
3237
+ };
3238
+ }
3239
+
3240
+ // src/realtime.ts
3241
+ var WS_OPEN = 1;
3242
+ var RealtimeChannel = class {
3243
+ #options;
3244
+ #socket = null;
3245
+ #opening = false;
3246
+ #authenticated = false;
3247
+ #closedByCaller = false;
3248
+ #backoffAttempt = 0;
3249
+ #reconnectTimer = null;
3250
+ #reauthAttempted = false;
3251
+ #identity = null;
3252
+ #connectionEpoch = 0;
3253
+ #frameListeners = /* @__PURE__ */ new Map();
3254
+ #readyListeners = /* @__PURE__ */ new Set();
3255
+ #authFailedListeners = /* @__PURE__ */ new Set();
3256
+ constructor(options) {
3257
+ this.#options = options;
3258
+ }
3259
+ get isReady() {
3260
+ return this.#authenticated;
3261
+ }
3262
+ get identity() {
3263
+ return this.#identity;
3264
+ }
3265
+ /**
3266
+ * Increments on every successful authentication (first connect and every
3267
+ * reconnect). A command sent on one physical socket can only ever be
3268
+ * answered on that socket — comparing the epoch captured at send time
3269
+ * against the current one is how a caller (see `commands.ts`) tells "still
3270
+ * waiting on the connection it was sent over" from "that connection is
3271
+ * gone and a new one has taken its place", the moment it happens rather
3272
+ * than after a timeout elapses.
3273
+ */
3274
+ get connectionEpoch() {
3275
+ return this.#connectionEpoch;
3276
+ }
3277
+ /**
3278
+ * Opens the socket if not already open/opening. Idempotent.
3279
+ *
3280
+ * `#opening` closes a real race: `#open()` is async and does not assign
3281
+ * `#socket` until *after* `await credentials.token()` — so two `connect()`
3282
+ * calls issued before that await settles (e.g. a `subscribe()` and a
3283
+ * command in the same tick, or two calls in a `Promise.all`) both used to
3284
+ * see `#socket` as unset and both proceed, opening two physical sockets.
3285
+ * Each socket's own `auth_ok` bumps `connectionEpoch`, so the second
3286
+ * socket authenticating made `sendCommand`'s dead-connection watch treat
3287
+ * the *first* socket — which was fine, and may already have carried a
3288
+ * command — as replaced, failing it `command_outcome_unknown` for a
3289
+ * connection that never actually died. The orphaned first socket was also
3290
+ * never closed: `close()`/`logout()` only ever reach whichever socket
3291
+ * `#socket` currently points to. `#opening` is set as the very first
3292
+ * statement inside `#open()` (not here — `#open()` is also called
3293
+ * directly from the reconnect timer and the re-auth retry, and both need
3294
+ * the same protection against a `connect()` landing in their own async
3295
+ * gap), so it is already true by the time any *synchronous* second caller
3296
+ * runs, because `void this.#open()` executes an async function's body up
3297
+ * to its first `await` immediately, not on a later microtask.
3298
+ */
3299
+ connect() {
3300
+ if (this.#socket || this.#reconnectTimer || this.#opening) return;
3301
+ this.#closedByCaller = false;
3302
+ void this.#open();
3303
+ }
3304
+ /**
3305
+ * Sends a frame only once the current socket has actually authenticated —
3306
+ * otherwise a silent no-op (nothing server-side to address). Checking the
3307
+ * socket's own `readyState` in addition to the channel-level
3308
+ * `#authenticated` flag is deliberate belt-and-suspenders: `#authenticated`
3309
+ * is set by whichever socket's `auth_ok` arrives, so if `#socket` were ever
3310
+ * reassigned without a matching reset (the `#opening` race above was one
3311
+ * way that could happen), a frame could otherwise be sent into a socket
3312
+ * that never authenticated at all.
3313
+ */
3314
+ send(frame) {
3315
+ if (this.#authenticated && this.#socket?.readyState === WS_OPEN) this.#socket.send(JSON.stringify(frame));
3316
+ }
3317
+ /** Registers a listener for one frame `type` (e.g. `'datapoint'`, `'subscribe_error'`). */
3318
+ on(type, handler) {
3319
+ let set = this.#frameListeners.get(type);
3320
+ if (!set) {
3321
+ set = /* @__PURE__ */ new Set();
3322
+ this.#frameListeners.set(type, set);
3323
+ }
3324
+ set.add(handler);
3325
+ return () => set.delete(handler);
3326
+ }
3327
+ /**
3328
+ * Fires once now if the channel is already authenticated, and again after
3329
+ * every future (re)authentication. This is how a subscription (re)sends
3330
+ * its `subscribe` frame without special-casing "already connected" itself.
3331
+ */
3332
+ onReady(fn) {
3333
+ this.#readyListeners.add(fn);
3334
+ if (this.#authenticated) queueMicrotask(fn);
3335
+ return () => this.#readyListeners.delete(fn);
3336
+ }
3337
+ /** Fires when authentication fails in a way that will not be retried (see `#handleAuthError`). */
3338
+ onAuthFailed(fn) {
3339
+ this.#authFailedListeners.add(fn);
3340
+ return () => this.#authFailedListeners.delete(fn);
3341
+ }
3342
+ /** Closes the socket and stops reconnecting. */
3343
+ close() {
3344
+ this.#closedByCaller = true;
3345
+ if (this.#reconnectTimer) {
3346
+ clearTimeout(this.#reconnectTimer);
3347
+ this.#reconnectTimer = null;
3348
+ }
3349
+ this.#authenticated = false;
3350
+ this.#socket?.close();
3351
+ this.#socket = null;
3352
+ }
3353
+ async #open() {
3354
+ this.#opening = true;
3355
+ const WebSocketCtor = this.#options.WebSocket;
3356
+ if (!WebSocketCtor) {
3357
+ this.#opening = false;
3358
+ this.#failAuth(new FleetlessError("no_websocket", "No WebSocket implementation is available in this environment."));
3359
+ return;
3360
+ }
3361
+ let token;
3362
+ try {
3363
+ token = await this.#options.credentials.token();
3364
+ } catch (error) {
3365
+ this.#opening = false;
3366
+ this.#failAuth(error instanceof FleetlessError ? error : new FleetlessError("no_session", "Could not obtain a credential."));
3367
+ return;
3368
+ }
3369
+ if (!token) {
3370
+ this.#opening = false;
3371
+ this.#failAuth(new FleetlessError("no_session", "Not authenticated \u2014 call auth.login() before subscribing."));
3372
+ return;
3373
+ }
3374
+ const socket = new WebSocketCtor(this.#options.url);
3375
+ this.#socket = socket;
3376
+ this.#opening = false;
3377
+ this.#authenticated = false;
3378
+ socket.onopen = () => {
3379
+ const authFrame = { type: "auth", token };
3380
+ socket.send(JSON.stringify(authFrame));
3381
+ };
3382
+ socket.onmessage = (event) => this.#handleMessage(event.data);
3383
+ socket.onclose = () => this.#handleClose();
3384
+ socket.onerror = () => {
3385
+ };
3386
+ }
3387
+ #handleMessage(raw) {
3388
+ let frame;
3389
+ try {
3390
+ frame = JSON.parse(raw);
3391
+ } catch {
3392
+ return;
3393
+ }
3394
+ if (typeof frame?.type !== "string") return;
3395
+ if (frame.type === "auth_ok") {
3396
+ this.#handleAuthOk(frame);
3397
+ return;
3398
+ }
3399
+ if (frame.type === "auth_error") {
3400
+ void this.#handleAuthError(frame);
3401
+ return;
3402
+ }
3403
+ this.#frameListeners.get(frame.type)?.forEach((handler) => handler(frame));
3404
+ }
3405
+ #handleAuthOk(frame) {
3406
+ this.#authenticated = true;
3407
+ this.#identity = frame.identity;
3408
+ this.#backoffAttempt = 0;
3409
+ this.#reauthAttempted = false;
3410
+ this.#connectionEpoch += 1;
3411
+ this.#readyListeners.forEach((fn) => fn());
3412
+ }
3413
+ async #handleAuthError(frame) {
3414
+ if (frame.code === "token_expired" && !this.#reauthAttempted) {
3415
+ this.#reauthAttempted = true;
3416
+ try {
3417
+ await this.#options.credentials.handleExpired();
3418
+ this.#socket?.close();
3419
+ this.#socket = null;
3420
+ void this.#open();
3421
+ return;
3422
+ } catch (refreshError) {
3423
+ this.#failAuth(refreshError instanceof FleetlessError ? refreshError : new FleetlessError(frame.code, frame.message));
3424
+ return;
3425
+ }
3426
+ }
3427
+ this.#failAuth(new FleetlessError(frame.code, frame.message));
3428
+ }
3429
+ #failAuth(error) {
3430
+ this.#closedByCaller = true;
3431
+ this.#socket?.close();
3432
+ this.#socket = null;
3433
+ this.#authenticated = false;
3434
+ this.#authFailedListeners.forEach((fn) => fn(error));
3435
+ }
3436
+ #handleClose() {
3437
+ this.#authenticated = false;
3438
+ this.#socket = null;
3439
+ if (this.#closedByCaller) return;
3440
+ this.#scheduleReconnect();
3441
+ }
3442
+ #scheduleReconnect() {
3443
+ const initial = this.#options.initialBackoffMs ?? 500;
3444
+ const max = this.#options.maxBackoffMs ?? 3e4;
3445
+ const delay = Math.min(initial * 2 ** this.#backoffAttempt, max);
3446
+ this.#backoffAttempt += 1;
3447
+ this.#reconnectTimer = setTimeout(() => {
3448
+ this.#reconnectTimer = null;
3449
+ void this.#open();
3450
+ }, delay);
3451
+ }
3452
+ };
3453
+
3454
+ // src/services.ts
3455
+ var DEFAULT_RESULT_TIMEOUT_MS = 3e4;
3456
+ function isTerminal(state) {
3457
+ return state === "succeeded" || state === "failed" || state === "cancelled" || state === "lost";
3458
+ }
3459
+ function unwrap(job2) {
3460
+ if (job2.state === "succeeded") return job2.result;
3461
+ const error = job2.error;
3462
+ throw new FleetlessError(
3463
+ error?.code ?? job2.state,
3464
+ error?.message ?? `The service call ended in state '${job2.state}'.`,
3465
+ error?.details !== void 0 ? { details: error.details } : void 0
3466
+ );
3467
+ }
3468
+ function createServicesApi(transport, jobSubscriptions) {
3469
+ return {
3470
+ async call(robotId, slug2, params, options = {}) {
3471
+ const timeoutMs = resolveLocalWaitMs(options, DEFAULT_RESULT_TIMEOUT_MS);
3472
+ const deadline = Date.now() + timeoutMs;
3473
+ const initial = await transport.invoke(robotId, slug2, params, { timeoutMs, patienceMs: options.patienceMs });
3474
+ const initialJob = initial.job;
3475
+ if (!initialJob) {
3476
+ throw new FleetlessError("unexpected_response", `The server accepted the call to '${slug2}' but returned no job to track.`);
3477
+ }
3478
+ if (isTerminal(initialJob.state)) return unwrap(initialJob);
3479
+ const remainingMs = Math.max(0, deadline - Date.now());
3480
+ if (remainingMs === 0) {
3481
+ throw new FleetlessError("command_timeout", `Service '${slug2}' did not reach a terminal state within ${timeoutMs}ms.`);
3482
+ }
3483
+ return new Promise((resolve, reject) => {
3484
+ let settled = false;
3485
+ const timer = setTimeout(() => {
3486
+ settle(
3487
+ () => reject(new FleetlessError("command_timeout", `Service '${slug2}' did not reach a terminal state within ${timeoutMs}ms.`))
3488
+ );
3489
+ }, remainingMs);
3490
+ const subscription = jobSubscriptions.subscribe(robotId, slug2, {
3491
+ onJob(event) {
3492
+ if (event.job.id !== initialJob.id || !isTerminal(event.job.state)) return;
3493
+ settle(() => {
3494
+ try {
3495
+ resolve(unwrap(event.job));
3496
+ } catch (error) {
3497
+ reject(error);
3498
+ }
3499
+ });
3500
+ },
3501
+ onError(error) {
3502
+ settle(() => reject(error));
3503
+ }
3504
+ });
3505
+ function settle(fn) {
3506
+ if (settled) return;
3507
+ settled = true;
3508
+ clearTimeout(timer);
3509
+ subscription.unsubscribe();
3510
+ fn();
3511
+ }
3512
+ });
3513
+ }
3514
+ };
3515
+ }
3516
+
3517
+ // src/slug-subscriptions.ts
3518
+ function keyOf3(robotId, slug2) {
3519
+ return `${robotId} ${slug2}`;
3520
+ }
3521
+ function createSlugSubscriptions(channel) {
3522
+ const registry = /* @__PURE__ */ new Map();
3523
+ channel.on("subscribe_error", (frame) => {
3524
+ const error = frame;
3525
+ const state = registry.get(keyOf3(error.robot_id, error.slug));
3526
+ if (!state) return;
3527
+ const fleetlessError = new FleetlessError(error.code, error.message);
3528
+ for (const callbacks of state.callbacks) callbacks.onSubscribeError(fleetlessError);
3529
+ });
3530
+ channel.onAuthFailed((error) => {
3531
+ for (const state of registry.values()) {
3532
+ for (const callbacks of state.callbacks) callbacks.onAuthFailed(error);
3533
+ }
3534
+ });
3535
+ return {
3536
+ acquire(robotId, slug2, callbacks) {
3537
+ const key = keyOf3(robotId, slug2);
3538
+ let state = registry.get(key);
3539
+ if (!state) {
3540
+ const unlistenReady = channel.onReady(() => {
3541
+ const frame = { type: "subscribe", robot_id: robotId, slug: slug2 };
3542
+ channel.send(frame);
3543
+ });
3544
+ state = { count: 0, callbacks: /* @__PURE__ */ new Set(), unlistenReady };
3545
+ registry.set(key, state);
3546
+ }
3547
+ state.count += 1;
3548
+ state.callbacks.add(callbacks);
3549
+ channel.connect();
3550
+ let released = false;
3551
+ return {
3552
+ release() {
3553
+ if (released) return;
3554
+ released = true;
3555
+ const current = registry.get(key);
3556
+ if (!current) return;
3557
+ current.callbacks.delete(callbacks);
3558
+ current.count -= 1;
3559
+ if (current.count > 0) return;
3560
+ current.unlistenReady();
3561
+ registry.delete(key);
3562
+ const frame = { type: "unsubscribe", robot_id: robotId, slug: slug2 };
3563
+ channel.send(frame);
3564
+ }
3565
+ };
3566
+ }
3567
+ };
3568
+ }
3569
+
3570
+ // src/token-store.ts
3571
+ var InMemoryTokenStore = class {
3572
+ #session = null;
3573
+ load() {
3574
+ return this.#session;
3575
+ }
3576
+ save(session) {
3577
+ this.#session = session;
3578
+ }
3579
+ };
3580
+
3581
+ // src/client.ts
3582
+ function createClient(options) {
3583
+ if (options.serverKey !== void 0 && options.tokenStore !== void 0) {
3584
+ throw new Error(
3585
+ "createClient: pass either `tokenStore` (end-user login) or `serverKey` (a server-side caller), not both."
3586
+ );
3587
+ }
3588
+ const config = Object.freeze({
3589
+ apiUrl: options.apiUrl,
3590
+ appIdentifier: options.appIdentifier,
3591
+ realtimeUrl: options.realtimeUrl ?? deriveRealtimeUrl(options.apiUrl)
3592
+ });
3593
+ const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
3594
+ if (!fetchImpl) {
3595
+ throw new Error("createClient: no global `fetch` is available in this environment \u2014 pass one explicitly.");
3596
+ }
3597
+ const webSocketImpl = options.WebSocket ?? globalThis.WebSocket;
3598
+ let auth;
3599
+ let http;
3600
+ let credentials;
3601
+ if (options.serverKey !== void 0) {
3602
+ credentials = new ServerKeyCredentials(options.serverKey);
3603
+ http = new HttpClient({ baseUrl: config.apiUrl, fetch: fetchImpl, credentials });
3604
+ auth = createServerKeyAuth(http, config.appIdentifier);
3605
+ } else {
3606
+ const tokenStore = options.tokenStore ?? new InMemoryTokenStore();
3607
+ http = new HttpClient({ baseUrl: config.apiUrl, fetch: fetchImpl, credentials: noCredentials });
3608
+ credentials = new SessionCredentials(http, tokenStore);
3609
+ http.setCredentials(credentials);
3610
+ auth = createSessionAuth(http, tokenStore, config.appIdentifier);
3611
+ }
3612
+ const channel = new RealtimeChannel({ url: config.realtimeUrl, WebSocket: webSocketImpl, credentials });
3613
+ const slugSubscriptions = createSlugSubscriptions(channel);
3614
+ const datapoints = createDatapointsApi(http, channel, slugSubscriptions);
3615
+ const commandTransport = createRealtimeCommandTransport(channel);
3616
+ const jobSubscriptions = createJobSubscriptions(channel, slugSubscriptions);
3617
+ const actions = createActionsApi(commandTransport, jobSubscriptions);
3618
+ const services = createServicesApi(commandTransport, jobSubscriptions);
3619
+ const publishers = createPublishersApi(commandTransport);
3620
+ const cameras = createCamerasApi(http);
3621
+ const jobs = createJobsApi(http);
3622
+ const assets = createAssetsApi(http);
3623
+ if (options.serverKey === void 0) {
3624
+ const baseLogout = auth.logout.bind(auth);
3625
+ auth = {
3626
+ ...auth,
3627
+ async logout() {
3628
+ const result = await baseLogout();
3629
+ channel.close();
3630
+ return result;
3631
+ }
3632
+ };
3633
+ }
3634
+ return {
3635
+ config,
3636
+ auth,
3637
+ datapoints,
3638
+ actions,
3639
+ services,
3640
+ publishers,
3641
+ cameras,
3642
+ jobs,
3643
+ assets,
3644
+ close() {
3645
+ channel.close();
3646
+ }
3647
+ };
3648
+ }
3649
+ function deriveRealtimeUrl(apiUrl) {
3650
+ const url = new URL(apiUrl);
3651
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
3652
+ url.pathname = `${url.pathname.replace(/\/$/, "")}/realtime`;
3653
+ return url.toString();
3654
+ }
3655
+ // Annotate the CommonJS export names for ESM import in node:
3656
+ 0 && (module.exports = {
3657
+ FleetlessError,
3658
+ InMemoryTokenStore,
3659
+ SDK_ERROR_CODES,
3660
+ createClient,
3661
+ parameterInvalidDetails
3662
+ });