@fleetless/contracts 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -2
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +136 -0
- package/README.md +49 -13
- package/SECURITY.md +55 -0
- package/artifacts/openapi.json +11 -11
- package/artifacts/routes.json +12 -12
- package/artifacts/schema/create-app-oidc-provider-request.schema.json +1 -1
- package/dist/alerts.d.ts +23 -28
- package/dist/alerts.js +23 -29
- package/dist/app-users.d.ts +18 -19
- package/dist/app-users.js +18 -20
- package/dist/apps.d.ts +21 -25
- package/dist/apps.js +42 -52
- package/dist/assets.d.ts +70 -132
- package/dist/assets.js +130 -223
- package/dist/audit.d.ts +14 -15
- package/dist/audit.js +28 -55
- package/dist/client-auth.d.ts +9 -9
- package/dist/client-auth.js +8 -9
- package/dist/common.d.ts +29 -37
- package/dist/common.js +28 -37
- package/dist/config-issues.d.ts +23 -25
- package/dist/config-issues.js +17 -17
- package/dist/config.d.ts +37 -44
- package/dist/config.js +145 -187
- package/dist/errors.d.ts +4 -3
- package/dist/errors.js +83 -116
- package/dist/identity.d.ts +24 -27
- package/dist/identity.js +23 -27
- package/dist/index.d.ts +4 -4
- package/dist/index.js +14 -15
- package/dist/introspection.d.ts +7 -6
- package/dist/introspection.js +6 -6
- package/dist/jobs.d.ts +16 -16
- package/dist/jobs.js +24 -29
- package/dist/mcp.d.ts +14 -15
- package/dist/mcp.js +12 -14
- package/dist/oauth.d.ts +21 -27
- package/dist/oauth.js +33 -43
- package/dist/protocol.d.ts +51 -62
- package/dist/protocol.js +107 -139
- package/dist/realtime.d.ts +53 -68
- package/dist/realtime.js +78 -104
- package/dist/rest.d.ts +183 -244
- package/dist/rest.js +305 -399
- package/dist/routes.d.ts +4 -3
- package/dist/routes.js +33 -32
- package/package.json +12 -7
package/dist/assets.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
/**
|
|
4
|
-
* The asset store
|
|
4
|
+
* The asset store.
|
|
5
5
|
*
|
|
6
6
|
* An **asset is an immutable file belonging to a robot**. It has a uuid and is
|
|
7
7
|
* fetched by it. There are no org-level assets and no public retrieval: every
|
|
8
|
-
* read is authenticated and
|
|
8
|
+
* read is authenticated and checked against the `assets` role.
|
|
9
9
|
*
|
|
10
10
|
* **One authorization model — the `Authorization` header.** Not a signed URL,
|
|
11
11
|
* not a cookie, not a token in a query string. The reason is not ergonomics
|
|
@@ -13,7 +13,7 @@ import { z } from 'zod';
|
|
|
13
13
|
* lifetime, its own renewal, its own rotation, and in every log the question
|
|
14
14
|
* of which token that was. Directus solves the same problem with a cookie and
|
|
15
15
|
* an `access_token` query parameter, and the cookie half does not transfer —
|
|
16
|
-
* Fleetless has no interface of its own
|
|
16
|
+
* Fleetless has no end-user interface of its own, so the consumers of a robot's
|
|
17
17
|
* assets sit on other origins.
|
|
18
18
|
*
|
|
19
19
|
* **The consequence, said out loud: this is not a CDN.** A shared cache must
|
|
@@ -26,10 +26,9 @@ import { z } from 'zod';
|
|
|
26
26
|
*
|
|
27
27
|
* ---
|
|
28
28
|
*
|
|
29
|
-
* **`texture` is its own member of `assetKind` and not `other
|
|
29
|
+
* **`texture` is its own member of `assetKind` and not `other`.**
|
|
30
30
|
*
|
|
31
|
-
* Filing textures under `other`
|
|
32
|
-
* already split five times, and it costs a real capability: a client that
|
|
31
|
+
* Filing textures under `other` costs a real capability: a client that
|
|
33
32
|
* renders a robot must know, from the asset list alone and before fetching
|
|
34
33
|
* anything, which bytes it has to pre-fetch. Every load in the browser goes
|
|
35
34
|
* through the SDK with the bearer token — there is no lazy second fetch a
|
|
@@ -79,7 +78,7 @@ export const asset = z.object({
|
|
|
79
78
|
* against their own workspace, and matching is the whole job when a sync
|
|
80
79
|
* comes back incomplete.
|
|
81
80
|
*
|
|
82
|
-
* **The naming rule for a file nothing in the URDF names
|
|
81
|
+
* **The naming rule for a file nothing in the URDF names.** A
|
|
83
82
|
* `.dae` carries its own image references — `<init_from>textures/skin.png`
|
|
84
83
|
* — resolved by the renderer against *the `.dae`'s own directory*, and no
|
|
85
84
|
* `package://` URI for them appears anywhere in the URDF. The rule is:
|
|
@@ -87,12 +86,11 @@ export const asset = z.object({
|
|
|
87
86
|
* name = the .dae's package:// URI, directory part,
|
|
88
87
|
* joined with the internal reference, normalized.
|
|
89
88
|
*
|
|
90
|
-
* So `package://
|
|
89
|
+
* So `package://robot_description/meshes/arm.dae` referencing
|
|
91
90
|
* `textures/skin.png` uploads as
|
|
92
|
-
* `package://
|
|
91
|
+
* `package://robot_description/meshes/textures/skin.png`.
|
|
93
92
|
*
|
|
94
|
-
* **
|
|
95
|
-
* anything is built against it.** three.js resolves that internal reference
|
|
93
|
+
* **Renderers depend on this rule holding.** three.js resolves that internal reference
|
|
96
94
|
* relative to wherever it loaded the `.dae` from and asks the loading
|
|
97
95
|
* manager for the result; the client can only answer if the asset's name
|
|
98
96
|
* still carries the same **relative tail** (`textures/skin.png`) that the
|
|
@@ -103,9 +101,8 @@ export const asset = z.object({
|
|
|
103
101
|
*
|
|
104
102
|
* A reference that escapes its package (`../../etc/passwd`) is **not**
|
|
105
103
|
* renamed into something harmless — it is refused at the producer, by the
|
|
106
|
-
* same containment check
|
|
107
|
-
*
|
|
108
|
-
* documented, is how W7's traversal happened in the first place.
|
|
104
|
+
* same containment check that guards `package://` resolution. Two identical
|
|
105
|
+
* rules, one enforced and one only documented, is how a traversal gets in.
|
|
109
106
|
*/
|
|
110
107
|
name: z.string().min(1).max(500).meta({
|
|
111
108
|
description: 'What the robot called it — for a mesh, the `package://` URI the URDF references, verbatim, which is the only string a developer can match against their own workspace. A file the URDF never names (an image a `.dae` loads for itself) is named by joining the mesh\'s own directory with that internal reference.',
|
|
@@ -137,23 +134,18 @@ export const asset = z.object({
|
|
|
137
134
|
*
|
|
138
135
|
* `missing` carries **the reference, verbatim, that no asset answers** — for
|
|
139
136
|
* a `package://` mesh the URI the bridge could not resolve in the workspace,
|
|
140
|
-
* and
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* producers, not on this field (W7a).** An entry a developer cannot make
|
|
153
|
-
* disappear by fixing what it names is a defect in whoever put it there: for
|
|
154
|
-
* a whole wave `<texture>` references were listed here and no sync would ever
|
|
155
|
-
* offer them, so the honest instruction behind the list was "fix this, it
|
|
156
|
-
* will not help".
|
|
137
|
+
* and also the absolute paths and bare relative paths a URDF may carry, which
|
|
138
|
+
* the extractor sees and the sync deliberately never offers. A developer whose
|
|
139
|
+
* URDF names `/opt/meshes/arm.stl` is entitled to be told that nothing will
|
|
140
|
+
* ever fetch it.
|
|
141
|
+
*
|
|
142
|
+
* A bare count of what is missing is a dead end: it tells a developer to go
|
|
143
|
+
* looking through a workspace by hand. The references are what they can act
|
|
144
|
+
* on, so the references travel.
|
|
145
|
+
*
|
|
146
|
+
* **Every entry must be actionable, and that is a constraint on the producers,
|
|
147
|
+
* not on this field.** An entry a developer cannot make disappear by fixing
|
|
148
|
+
* what it names is a defect in whoever put it there.
|
|
157
149
|
*/
|
|
158
150
|
export const urdfCompleteness = z.object({
|
|
159
151
|
present: z.boolean().meta({
|
|
@@ -163,18 +155,17 @@ export const urdfCompleteness = z.object({
|
|
|
163
155
|
description: 'How many distinct meshes the URDF references.',
|
|
164
156
|
}),
|
|
165
157
|
/**
|
|
166
|
-
* **
|
|
158
|
+
* **What is missing, and what kind of thing it was.**
|
|
167
159
|
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* `mesh_count`
|
|
171
|
-
*
|
|
160
|
+
* Each entry carries its element rather than only its URI. Without that a
|
|
161
|
+
* client can only report every entry as a missing mesh, which contradicts
|
|
162
|
+
* `mesh_count` printed beside it — two statements about the same subject
|
|
163
|
+
* that disagree.
|
|
172
164
|
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* Herleitung derselben Tatsache, die von der ersten abweichen kann.
|
|
165
|
+
* The producer knows the element when it extracts the reference, so it
|
|
166
|
+
* travels with it. A client that re-derived it from the file extension
|
|
167
|
+
* would be a second derivation of the same fact, free to diverge from the
|
|
168
|
+
* first.
|
|
178
169
|
*/
|
|
179
170
|
missing: z.array(z.object({
|
|
180
171
|
uri: z.string().min(1).max(500).meta({
|
|
@@ -191,21 +182,14 @@ export const urdfCompleteness = z.object({
|
|
|
191
182
|
* A sync is long-running and is therefore answered with something to watch,
|
|
192
183
|
* never with a status that was true at the moment of asking.
|
|
193
184
|
*
|
|
194
|
-
* **`source` has one value, and that is deliberate.**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* differently; that is a defect this project has deliberately refused to
|
|
199
|
-
* introduce before, when an error code was proposed whose payload had moved.
|
|
200
|
-
* The zip path stays a condition rather than sitting in the wire as a
|
|
201
|
-
* promise.
|
|
185
|
+
* **`source` has one value, and that is deliberate.** A manual upload path is
|
|
186
|
+
* planned but has no body defined for the bytes yet. An enum value with no
|
|
187
|
+
* producer and no payload invites every consumer to guess a shape, and each
|
|
188
|
+
* guesses differently, so it stays out of the wire until it is real.
|
|
202
189
|
*
|
|
203
|
-
* A single-member enum rather than
|
|
190
|
+
* A single-member enum rather than no field at all: the second source is a
|
|
204
191
|
* question of when, not whether, and a caller that already names its source
|
|
205
192
|
* does not change shape when the second one arrives.
|
|
206
|
-
*
|
|
207
|
-
* Caught by Eve-W7 asking what body `'upload'` takes, rather than building
|
|
208
|
-
* against a guess.
|
|
209
193
|
*/
|
|
210
194
|
export const assetSyncRequest = z.object({
|
|
211
195
|
source: z.enum(['bridge']).meta({
|
|
@@ -225,83 +209,43 @@ export const assetSyncResponse = z.object({
|
|
|
225
209
|
* worse than one that fails outright, because the failure surfaces later, in a
|
|
226
210
|
* renderer, as a robot with missing limbs and no explanation.
|
|
227
211
|
*
|
|
228
|
-
* **`failed` carries per-reference facts and `reason` carries the sync's
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* assume one. Read together with the old sentence *"`failed` is URIs and
|
|
245
|
-
* nothing else"*, this file told a consumer the same value was both a defect
|
|
246
|
-
* and a documented case.
|
|
247
|
-
*
|
|
248
|
-
* So the rule is: **anything that is not about one specific reference goes in
|
|
249
|
-
* `reason`** — one human-readable sentence
|
|
250
|
-
* about why the sync ended as it did, `null` when the outcome speaks for
|
|
251
|
-
* itself. It also carries the distinction `bridgeAssetProgress.state` makes
|
|
252
|
-
* and this shape could not — a sync **refused** because another was in flight
|
|
253
|
-
* is not a sync that tried and failed.
|
|
254
|
-
*
|
|
255
|
-
* **`assetSyncState` was deliberately not widened to carry that.** Consumers
|
|
256
|
-
* switch on it, a new member silently changes what every existing switch
|
|
257
|
-
* covers, and "refused" is a *reason* for a terminal outcome rather than a
|
|
258
|
-
* different one. Adding a field is additive; adding an enum member is not.
|
|
212
|
+
* **`failed` carries per-reference facts and `reason` carries the sync's
|
|
213
|
+
* own.** Anything that is not about one specific reference goes in `reason` —
|
|
214
|
+
* one human-readable sentence about why the sync ended as it did, `null` when
|
|
215
|
+
* the outcome speaks for itself. A whole-sync condition written into `failed`
|
|
216
|
+
* reaches a developer as a mesh they are told to go and find.
|
|
217
|
+
*
|
|
218
|
+
* Not every `failed` entry is a mesh URI. A URDF upload can fail like any
|
|
219
|
+
* other asset, and it appears under the name `robot_description`; see
|
|
220
|
+
* `assetFailure.reference`. A consumer must not assume every entry is a
|
|
221
|
+
* `package://` URI.
|
|
222
|
+
*
|
|
223
|
+
* **`assetSyncState` is deliberately not widened to carry the reason.**
|
|
224
|
+
* Consumers switch on it, and a new member silently changes what every
|
|
225
|
+
* existing switch covers. A sync refused because another was in flight is
|
|
226
|
+
* still a terminal outcome with a reason, not a different state. Adding a
|
|
227
|
+
* field is additive; adding an enum member is not.
|
|
259
228
|
*/
|
|
260
229
|
/**
|
|
261
|
-
* **
|
|
230
|
+
* **The upload ceiling both sides read.**
|
|
262
231
|
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* die der Sender rät und der Empfänger durchsetzt, ist keine Grenze — sie ist
|
|
267
|
-
* zwei Zahlen, die zufällig übereinstimmen, bis eine von beiden sich ändert.
|
|
232
|
+
* It is stated once, here, rather than once in the producer and once in the
|
|
233
|
+
* cloud. A limit the sender guesses and the receiver enforces is not a limit;
|
|
234
|
+
* it is two numbers that agree until one of them changes.
|
|
268
235
|
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* ist (DEF-148).
|
|
236
|
+
* The bridge reads it **before** it reads a file into memory, and the cloud
|
|
237
|
+
* enforces it. Without a shared number a producer cannot refuse an oversized
|
|
238
|
+
* mesh without first buffering the whole of it.
|
|
273
239
|
*
|
|
274
|
-
* **
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
* anderen Stelle.
|
|
240
|
+
* **It applies per file, not per sync.** Eight meshes of 30 MiB each pass; one
|
|
241
|
+
* file of 65 MiB does not. Reading it as a ceiling on a whole transfer means
|
|
242
|
+
* planning against a bound that does not exist — the total of a sync counts
|
|
243
|
+
* against the organisation's storage quota, which is a different number in a
|
|
244
|
+
* different place.
|
|
280
245
|
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* Zuerst stand hier: *„rx1s echte Meshes sind gemessen — `base.dae`
|
|
285
|
-
* 193.886.766 Bytes, also das 2,9-fache"*, im Präsens, als stünde das über
|
|
286
|
-
* der heute laufenden Beschreibung. Gemessen am 2026-08-19 gegen die
|
|
287
|
-
* **laufende** `robot_description`: acht `package://`-Referenzen, zusammen
|
|
288
|
-
* 89.379.096 Bytes, die größte `RX1.dae` mit 38.229.621 — **keine über dem
|
|
289
|
-
* Deckel.**
|
|
290
|
-
*
|
|
291
|
-
* Daraus habe ich dann geschlossen, `base.dae` werde *von keiner* rx1-URDF
|
|
292
|
-
* referenziert. **Auch das war falsch, und zwar weil ich nur den aktuellen
|
|
293
|
-
* Workspace geprüft hatte.** `src.old-20260730/rx1` und `.../rx1_linac`
|
|
294
|
-
* referenzieren beide `base.dae` **und** `base.stl` und kennen `RX1.dae`
|
|
295
|
-
* nicht — das ist die Beschreibung, die rx1 am 2026-08-18 lief, als DEF-148
|
|
296
|
-
* gemessen wurde. Die Beobachtung von damals war korrekt und ihre Erklärung
|
|
297
|
-
* auch.
|
|
298
|
-
*
|
|
299
|
-
* Was heute gilt: **welche Beschreibung rx1 fährt, entscheidet, ob der Deckel
|
|
300
|
-
* reicht** — die aktuelle passt mit Abstand hinein, die vorherige um das
|
|
301
|
-
* 2,9-fache nicht. Das ist keine Vertragsfrage, sondern eine über Speicher,
|
|
302
|
-
* Übertragungszeit und Kontingente, und sie liegt bei André. Sie blockiert
|
|
303
|
-
* nichts: W9bs Gate-Schritt 6 ist gegen die heute laufende Beschreibung
|
|
304
|
-
* erreichbar, ohne dass jemand eine Zahl anfasst.
|
|
246
|
+
* A robot whose meshes exceed this is not a contract question but a question
|
|
247
|
+
* about storage, transfer time and quota, and it is answered by raising the
|
|
248
|
+
* number here, in one place, for both sides.
|
|
305
249
|
*/
|
|
306
250
|
export const ASSET_UPLOAD_MAX_BYTES = 64 * 1024 * 1024;
|
|
307
251
|
export const assetTooLargeDetails = z.object({
|
|
@@ -326,9 +270,9 @@ export const assetTooLargeDetails = z.object({
|
|
|
326
270
|
* - **`upload_failed`** — the bytes exist and the transfer did not succeed.
|
|
327
271
|
* **Transient.** The asset is still wanted; a later sync will carry it.
|
|
328
272
|
* - **`refused`** — never attempted, because a producer-side ceiling was hit
|
|
329
|
-
* (
|
|
330
|
-
* report). **Transient in the same sense**: nothing is known
|
|
331
|
-
* only unexamined.
|
|
273
|
+
* (for example a `.dae` carrying more internal references than one file or
|
|
274
|
+
* one sync will report). **Transient in the same sense**: nothing is known
|
|
275
|
+
* to be missing, only unexamined.
|
|
332
276
|
*
|
|
333
277
|
* A consumer that cannot act on the distinction may still print `reference`
|
|
334
278
|
* alone and lose nothing it had before.
|
|
@@ -349,31 +293,28 @@ export const assetFailure = z.object({
|
|
|
349
293
|
description: 'Why it failed. `unresolvable` means the reference names nothing the producer can find or may read, and is **permanent** — the only kind reconciliation may treat as gone. `upload_failed` means the bytes exist and the transfer did not succeed, `refused` means it was never attempted because a producer-side ceiling was hit, and `too_large` means it exceeds the upload limit and carries both numbers in `details`.',
|
|
350
294
|
}),
|
|
351
295
|
/**
|
|
352
|
-
* **
|
|
296
|
+
* **The two numbers, and why `too_large` is a kind of its own.**
|
|
353
297
|
*
|
|
354
|
-
* `refused`
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
* derselbe Fehler, den W9a eine Welle zuvor ausgeräumt hat: zwei Fakten auf
|
|
359
|
-
* einem Schlüssel, von denen jeder den anderen überschreibt.
|
|
298
|
+
* `refused` means *never attempted, because a producer-side ceiling was
|
|
299
|
+
* hit*. That fits a file skipped for its size **and** the single collective
|
|
300
|
+
* entry a sync emits when it stops naming individual failures. Filing both
|
|
301
|
+
* under one kind puts two facts on one key, each overwriting the other.
|
|
360
302
|
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
303
|
+
* A reason without numbers is not one a caller can act on. *"Too large"*
|
|
304
|
+
* does not answer whether to shrink the mesh or raise the limit;
|
|
305
|
+
* `limit_bytes` and `size_bytes` do.
|
|
364
306
|
*
|
|
365
|
-
*
|
|
366
|
-
* `unresolvable`
|
|
367
|
-
*
|
|
368
|
-
* Bitte.
|
|
307
|
+
* Absent for every other kind — a forced `details: null` on every
|
|
308
|
+
* `unresolvable` buys nothing. The pairing is **enforced** below, not merely
|
|
309
|
+
* described: a field whose rule lives only in a comment is a request.
|
|
369
310
|
*/
|
|
370
311
|
details: assetTooLargeDetails.nullish().meta({
|
|
371
312
|
description: 'The two numbers behind a `too_large` failure, and absent for every other kind — a forced `null` on every `unresolvable` entry buys nothing. The pairing is enforced, not merely described.',
|
|
372
313
|
}),
|
|
373
314
|
}).superRefine((f, ctx) => {
|
|
374
|
-
// **
|
|
375
|
-
//
|
|
376
|
-
//
|
|
315
|
+
// **Enforced, not merely described.** A rule that lives only in a comment is
|
|
316
|
+
// a request, and a field whose meaning sits beside it rather than in it gets
|
|
317
|
+
// filled with something else.
|
|
377
318
|
if (f.kind === 'too_large' && f.details == null) {
|
|
378
319
|
ctx.addIssue({ code: 'custom', path: ['details'], message: '`too_large` without limit_bytes/size_bytes says nothing a developer can act on' });
|
|
379
320
|
}
|
|
@@ -395,48 +336,28 @@ export const assetSyncStatus = z.object({
|
|
|
395
336
|
description: 'How many files this sync set out to transfer. It is `0` until the producer has finished working out what there is.',
|
|
396
337
|
}),
|
|
397
338
|
/**
|
|
398
|
-
* **Every entry says *why
|
|
399
|
-
* it and a developer could not read it without it** (W7a review, André's
|
|
400
|
-
* decision to fix rather than defer).
|
|
339
|
+
* **Every entry says *why*.**
|
|
401
340
|
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
* ceiling — one that was never attempted at all. The cost was paid twice
|
|
406
|
-
* over. Reconciliation cannot tell *"no longer referenced"* from
|
|
407
|
-
* *"referenced and not delivered"*, so N14 had to decline reconciling **any**
|
|
408
|
-
* partial sync, leaving legitimately-removed assets stored and charged until
|
|
409
|
-
* the next clean one. And the console prints the whole array under *"these
|
|
410
|
-
* meshes could not be resolved"*, so a URDF upload failure — which arrives
|
|
411
|
-
* as the literal `robot_description` — is shown to a developer as a mesh
|
|
412
|
-
* they should go and find.
|
|
341
|
+
* A flat `string[]` cannot carry three different facts distinguishably: a
|
|
342
|
+
* reference that resolves to nothing in the workspace, a file that exists
|
|
343
|
+
* and whose transfer failed, and one that was never attempted at all.
|
|
413
344
|
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
345
|
+
* The distinction is what makes reconciliation possible. `unresolvable` is
|
|
346
|
+
* the only kind that means *this will not come back*, so it is the only kind
|
|
347
|
+
* an unreferenced-asset sweep may act on. `upload_failed` and `refused` both
|
|
348
|
+
* mean *this was meant to be provided and was not*, and dropping the asset
|
|
349
|
+
* on either would delete something still wanted.
|
|
418
350
|
*
|
|
419
|
-
* **Bounded,
|
|
420
|
-
*
|
|
421
|
-
*
|
|
351
|
+
* **Bounded, per entry and in total.** One mesh reference can expand into a
|
|
352
|
+
* list bounded only by the text of the `.dae` it points at, and the whole
|
|
353
|
+
* status travels in one WebSocket frame with a maximum payload. Without a
|
|
354
|
+
* cap the outcome is not a dropped frame but the robot's socket closed
|
|
355
|
+
* mid-sync by a file in its own workspace. At most 1000 entries of at most
|
|
356
|
+
* 500 bytes is about 0.5 MiB of names, comfortably inside that frame.
|
|
422
357
|
*
|
|
423
|
-
*
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
* `.dae`'s internal references are reported, **one mesh reference expands
|
|
427
|
-
* into a list bounded only by that file's own text.** Measured: a
|
|
428
|
-
* 2,120,745-byte `.dae` with 17,331 unresolvable `<init_from>` refs produces
|
|
429
|
-
* a terminal frame of 2,097,184 bytes — **32 bytes over
|
|
430
|
-
* `MAX_WS_PAYLOAD_BYTES`** — and `ws` enforces `maxPayload` before the frame
|
|
431
|
-
* is delivered, so the outcome is not a dropped frame but **the robot's
|
|
432
|
-
* socket closed, mid-sync, by a file in its own workspace.**
|
|
433
|
-
*
|
|
434
|
-
* A producer that hits its own ceiling reports **one** entry saying so
|
|
435
|
-
* rather than growing the list — the discipline gate step 5 already demands
|
|
436
|
-
* of the upload rate limit: *fail naming the limit, rather than silently
|
|
437
|
-
* reporting resolvable meshes as missing.*
|
|
438
|
-
*
|
|
439
|
-
* 1000 x 500 bytes is ~0.5 MiB of names, comfortably inside a 2 MiB frame.
|
|
358
|
+
* A producer that reaches its own ceiling reports **one** entry saying so
|
|
359
|
+
* rather than growing the list: fail naming the limit, rather than silently
|
|
360
|
+
* reporting resolvable meshes as missing.
|
|
440
361
|
*/
|
|
441
362
|
failed: z.array(assetFailure).max(1000).meta({
|
|
442
363
|
description: 'What could not be provided, one entry per reference, each saying why. Required rather than optional: a sync that quietly drops three meshes and reports success moves the failure into somebody\'s renderer, where it shows up as a robot with missing limbs and no cause. At most `1000` entries — a producer at its own ceiling reports one entry saying so rather than growing the list.',
|
|
@@ -456,14 +377,13 @@ export const assetListResponse = z.object({
|
|
|
456
377
|
description: 'Every asset stored for this robot: the URDF, the meshes it references, and the textures those paint with.',
|
|
457
378
|
}),
|
|
458
379
|
/**
|
|
459
|
-
*
|
|
380
|
+
* The sync running right now, or `null`.
|
|
460
381
|
*
|
|
461
|
-
* **
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
* Knopf; sie fragt diese Liste. Also muss die Liste es sagen.
|
|
382
|
+
* **This field exists for the reload case.** A client that holds the
|
|
383
|
+
* `sync_id` only in memory loses its progress display on a refresh, and the
|
|
384
|
+
* state is still there server-side under `GET .../assets/sync/<id>` —
|
|
385
|
+
* unreachable to anyone who did not keep the id. A page that loads fresh
|
|
386
|
+
* presses no button; it asks this list, so this list has to say.
|
|
467
387
|
*/
|
|
468
388
|
active_sync: assetSyncStatus.nullable().meta({
|
|
469
389
|
description: 'The sync running right now, or `null`. It is on this list so a page that reloads and has lost the sync id can still show progress — a freshly loaded page presses no button, it asks this list.',
|
|
@@ -473,30 +393,20 @@ export const assetListResponse = z.object({
|
|
|
473
393
|
}),
|
|
474
394
|
/**
|
|
475
395
|
* What the connected bridge says it *could* transfer, which is deliberately
|
|
476
|
-
* separate from what has been transferred
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
* send a developer to two different places.
|
|
480
|
-
*
|
|
481
|
-
* **All three states are reachable as of W7a (R7).** They were not: the bridge
|
|
482
|
-
* used to report availability from a subscription callback, which fires only
|
|
483
|
-
* when a publisher *sends* something, so it could notice presence and never
|
|
484
|
-
* absence — a robot that lost its URDF left the cloud holding the last thing
|
|
485
|
-
* it heard, forever, and `true` was sticky. The fix is an **active**
|
|
486
|
-
* `count_publishers` query on the bridge's own timer.
|
|
396
|
+
* separate from what has been transferred. `null` when no bridge is
|
|
397
|
+
* connected — distinct from `false`, because "no robot is online to ask" and
|
|
398
|
+
* "the robot has no URDF" send a developer to two different places.
|
|
487
399
|
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
490
|
-
* noticed
|
|
491
|
-
* interval. Measured against a real bridge: ~1.6 s when the publisher calls
|
|
492
|
-
* `destroy_node()`, **~19 s when it is `SIGKILL`ed**. So `true` can outlive
|
|
493
|
-
* the truth by some seconds after a crash, and no amount of polling on our
|
|
494
|
-
* side shortens it.
|
|
400
|
+
* The bridge answers by actively counting publishers on its own timer, so
|
|
401
|
+
* both the appearance and the disappearance of a robot description are
|
|
402
|
+
* noticed. A callback-driven answer can only see presence.
|
|
495
403
|
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
404
|
+
* **What a consumer needs to know is the clock.** An ungraceful loss — the
|
|
405
|
+
* publisher process killed rather than shut down — is noticed on DDS's
|
|
406
|
+
* liveliness timeout, not on the bridge's check interval. A clean
|
|
407
|
+
* `destroy_node()` is visible in a second or two; a killed process can take
|
|
408
|
+
* around twenty. So `true` can outlive the truth by some seconds after a
|
|
409
|
+
* crash, and no amount of polling shortens it.
|
|
500
410
|
*/
|
|
501
411
|
urdf_available: z.boolean().nullable().meta({
|
|
502
412
|
description: 'What the connected bridge says it *could* transfer, which is deliberately separate from what has been transferred. `null` when no bridge is connected — distinct from `false`, because "no robot is online to ask" and "the robot has no URDF" send a developer to two different places. After a publisher is killed rather than shut down this can read `true` for some seconds, on the underlying DDS liveliness timeout rather than on any check made here.',
|
|
@@ -525,22 +435,19 @@ export const missingAssetQuery = z
|
|
|
525
435
|
* number leaves the caller unable to decide anything.
|
|
526
436
|
*/
|
|
527
437
|
/**
|
|
528
|
-
*
|
|
438
|
+
* What a `busy` refusal on an asset sync has to carry.
|
|
529
439
|
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
* id nicht aufgehoben hatte. Genau die Form, die W6b eine ganze Welle lang
|
|
535
|
-
* ausgeräumt hat: ein Abbruch ohne Job-Id, eine Freigabe ohne Session-Id.
|
|
440
|
+
* A refusal that names a **state** — "a sync is already in progress" — and not
|
|
441
|
+
* the **thing** in that state leaves the caller with nothing to look at. The
|
|
442
|
+
* running sync is observable under `GET .../assets/sync/<id>`, but only to
|
|
443
|
+
* someone who kept its id.
|
|
536
444
|
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
* gar nichts und braucht den laufenden Sync im ersten `GET`.
|
|
445
|
+
* These details serve the caller that presses the button again. A client that
|
|
446
|
+
* reloads instead presses nothing, which is why `active_sync` sits on the
|
|
447
|
+
* asset list as well.
|
|
541
448
|
*/
|
|
542
449
|
export const assetSyncBusyDetails = z.object({
|
|
543
450
|
sync_id: z.uuid(),
|
|
544
|
-
/**
|
|
451
|
+
/** When it started, so "still running" is distinguishable from "stuck for an hour". */
|
|
545
452
|
started_at_ms: z.number().int().nonnegative(),
|
|
546
453
|
});
|
package/dist/audit.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
/**
|
|
3
|
-
|
|
4
|
-
*
|
|
4
|
+
/**
|
|
5
|
+
* Audit. Every state-changing interaction is recorded and **every entry carries
|
|
6
|
+
* its actor — never anonymous**. Reads are not audited.
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* 2026-08-10) — same shape of work as the history API.
|
|
8
|
+
* What is written: logins, failed logins, user management, configuration
|
|
9
|
+
* publishes, bridge connect and disconnect. The log is filterable, exportable
|
|
10
|
+
* as CSV, and kept for ninety days.
|
|
10
11
|
*/
|
|
11
12
|
/**
|
|
12
13
|
* The kinds of actor the platform knows. `label` is what a human reads in the
|
|
@@ -14,7 +15,7 @@ import { z } from 'zod';
|
|
|
14
15
|
* resolve four different id kinds to render a row.
|
|
15
16
|
*
|
|
16
17
|
* **`end_user` stays, and it stays for the rows already written.** The
|
|
17
|
-
* two
|
|
18
|
+
* split into two identity spaces replaced the org's one user pool with
|
|
18
19
|
* Fleetless users and per-app app users; every new row an app user writes
|
|
19
20
|
* carries `app_user`. But an audit log is the one thing this platform must
|
|
20
21
|
* never rewrite, and there are stored rows whose `kind` is `end_user`. Dropping
|
|
@@ -22,9 +23,8 @@ import { z } from 'zod';
|
|
|
22
23
|
* cannot be read back is worse than one carrying a retired word.
|
|
23
24
|
*
|
|
24
25
|
* So this enum is deliberately **wider than what any producer emits**: nothing
|
|
25
|
-
* writes `end_user` any more, and nothing may start again. That is
|
|
26
|
-
*
|
|
27
|
-
* is said here rather than inferred from a `grep` somebody runs in a year.
|
|
26
|
+
* writes `end_user` any more, and nothing may start again. That is said here
|
|
27
|
+
* rather than left to be inferred from a search somebody runs in a year.
|
|
28
28
|
*
|
|
29
29
|
* `developer` is a Fleetless user. It kept its name through both redesigns
|
|
30
30
|
* because it was always right about what it named: the person who configures
|
|
@@ -107,7 +107,7 @@ export declare const auditListResponse: z.ZodObject<{
|
|
|
107
107
|
}, z.core.$strip>;
|
|
108
108
|
export type AuditListResponse = z.infer<typeof auditListResponse>;
|
|
109
109
|
/**
|
|
110
|
-
* **What a CSV export of this log looks like
|
|
110
|
+
* **What a CSV export of this log looks like.**
|
|
111
111
|
*
|
|
112
112
|
* The column order lives here because otherwise the cloud and the console
|
|
113
113
|
* would each carry their own, and nobody would notice them drifting apart
|
|
@@ -120,10 +120,9 @@ export type AuditListResponse = z.infer<typeof auditListResponse>;
|
|
|
120
120
|
*/
|
|
121
121
|
export declare const AUDIT_CSV_COLUMNS: readonly ["seq", "at", "actor_kind", "actor_id", "action", "target_kind", "target_id", "target_label", "details"];
|
|
122
122
|
/**
|
|
123
|
-
*
|
|
123
|
+
* The audit log is kept for **90 days**.
|
|
124
124
|
*
|
|
125
|
-
* A constant here so
|
|
126
|
-
*
|
|
127
|
-
* there is no purge touching audit rows at all.
|
|
125
|
+
* A constant here so no consumer derives it a second time — the same reasoning
|
|
126
|
+
* as `ASSET_UPLOAD_MAX_BYTES`.
|
|
128
127
|
*/
|
|
129
128
|
export declare const AUDIT_RETENTION_DAYS = 90;
|