@fleetless/contracts 1.0.0 → 1.0.2

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +68 -2
  2. package/CODE_OF_CONDUCT.md +83 -0
  3. package/CONTRIBUTING.md +136 -0
  4. package/README.md +49 -13
  5. package/SECURITY.md +55 -0
  6. package/artifacts/openapi.json +1 -1
  7. package/artifacts/routes.json +1 -1
  8. package/dist/alerts.d.ts +19 -24
  9. package/dist/alerts.js +18 -24
  10. package/dist/app-users.d.ts +7 -6
  11. package/dist/app-users.js +6 -6
  12. package/dist/apps.d.ts +21 -25
  13. package/dist/apps.js +40 -51
  14. package/dist/assets.d.ts +70 -132
  15. package/dist/assets.js +130 -223
  16. package/dist/audit.d.ts +11 -11
  17. package/dist/audit.js +25 -51
  18. package/dist/client-auth.d.ts +4 -4
  19. package/dist/client-auth.js +3 -4
  20. package/dist/common.d.ts +27 -35
  21. package/dist/common.js +26 -35
  22. package/dist/config-issues.d.ts +4 -3
  23. package/dist/config-issues.js +7 -6
  24. package/dist/config.d.ts +31 -37
  25. package/dist/config.js +81 -110
  26. package/dist/errors.d.ts +4 -3
  27. package/dist/errors.js +61 -87
  28. package/dist/identity.d.ts +18 -21
  29. package/dist/identity.js +17 -21
  30. package/dist/index.d.ts +4 -4
  31. package/dist/index.js +12 -13
  32. package/dist/introspection.d.ts +7 -6
  33. package/dist/introspection.js +6 -6
  34. package/dist/jobs.d.ts +12 -12
  35. package/dist/jobs.js +20 -25
  36. package/dist/mcp.d.ts +11 -12
  37. package/dist/mcp.js +10 -12
  38. package/dist/oauth.d.ts +13 -18
  39. package/dist/oauth.js +13 -19
  40. package/dist/protocol.d.ts +51 -62
  41. package/dist/protocol.js +107 -139
  42. package/dist/realtime.d.ts +53 -68
  43. package/dist/realtime.js +77 -103
  44. package/dist/rest.d.ts +182 -243
  45. package/dist/rest.js +301 -395
  46. package/dist/routes.d.ts +4 -3
  47. package/dist/routes.js +3 -2
  48. package/package.json +12 -7
package/dist/errors.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  import { z } from 'zod';
3
3
  /**
4
- * The one error shape of the REST and realtime APIs (spec §11.5): a stable
4
+ * The one error shape of the REST and realtime APIs: a stable
5
5
  * machine-readable code plus a human message; validation errors name the
6
6
  * field and the violated rule in `details`.
7
7
  */
@@ -11,7 +11,7 @@ export const apiError = z.object({
11
11
  details: z.unknown().optional(),
12
12
  });
13
13
  /**
14
- * One violated §4.4 rule. `details` on the envelope stays `unknown` — codes
14
+ * One violated parameter rule. `details` on the envelope stays `unknown` — codes
15
15
  * are an open set, so their payloads cannot all be enumerated — but the
16
16
  * payload of `parameter_invalid` **is** pinned here, because otherwise every
17
17
  * consumer guesses: the cloud emits one shape, the SDK sniffs for two, the
@@ -39,12 +39,12 @@ export const parameterInvalidDetails = z.object({
39
39
  violations: z.array(parameterViolation).min(1),
40
40
  });
41
41
  /**
42
- * The codes in use as of W2. The wire deliberately allows any string — this
42
+ * The codes in use today. The wire deliberately allows any string — this
43
43
  * list is the shared vocabulary, not a closed set, so a new refusal never
44
44
  * needs a contracts release before it can be reported honestly.
45
45
  */
46
46
  export const ERROR_CODES = [
47
- // W1
47
+ // Core.
48
48
  'not_found',
49
49
  'validation_error',
50
50
  'bad_request',
@@ -52,7 +52,7 @@ export const ERROR_CODES = [
52
52
  'invalid_token',
53
53
  'protocol_mismatch',
54
54
  'invalid_frame',
55
- // W2 — configuration
55
+ // Configuration.
56
56
  'duplicate_slug',
57
57
  'reserved_slug',
58
58
  /**
@@ -67,14 +67,14 @@ export const ERROR_CODES = [
67
67
  'invalid_rate',
68
68
  'invalid_range',
69
69
  'config_conflict',
70
- // W2 — reading
70
+ // Reading.
71
71
  'no_data',
72
- // W2 — talking to the robot
72
+ // Talking to the robot.
73
73
  'robot_offline',
74
74
  'bridge_timeout',
75
- // W3 — identity and rights. `forbidden` is deliberately the answer both
75
+ // Identity and rights. `forbidden` is deliberately the answer both
76
76
  // for "your role does not grant this" and for "there is no such slug":
77
- // roles are the only filter (§3.3), and a caller must not be able to map
77
+ // roles are the only filter, and a caller must not be able to map
78
78
  // the configuration of an app they have no rights in.
79
79
  'unauthorized',
80
80
  'forbidden',
@@ -95,15 +95,11 @@ export const ERROR_CODES = [
95
95
  * project's list: a documented refusal no caller can receive, which a reader
96
96
  * would reasonably branch on. */
97
97
  /**
98
- * The address is already taken — **globally, across every org** (Andre,
99
- * 2026-08-29).
98
+ * The address is already taken — **globally, across every org**.
100
99
  *
101
- * The 2026-08-29 redesign first made `users.email` unique *per org* (D1), so
102
- * this code briefly meant only *this org already has this address*. That was
103
- * reversed the same day: email is **globally unique** again, one address is
104
- * exactly one account in exactly one org, and this code means *somebody,
105
- * somewhere already has this address* — the pre-redesign meaning the code's
106
- * name always implied. There is no per-org reading of it any more.
100
+ * A Fleetless user's email is globally unique: one address is exactly one
101
+ * account in exactly one org, and this code means *somebody, somewhere
102
+ * already has this address*. There is no per-org reading of it.
107
103
  *
108
104
  * It stays an answer to a *write* an authenticated caller made — signing up,
109
105
  * inviting or creating — never to a login, which may not say whether an
@@ -130,39 +126,25 @@ export const ERROR_CODES = [
130
126
  * tells the account holder something about *their own* account, and reveals
131
127
  * nothing about any other principal or about what exists.
132
128
  *
133
- * **It has now lost its producer, as this comment predicted it would.** The
134
- * paragraph here used to say "it loses its producer when D1's `users`
135
- * replaces `end_users` — it has not lost it yet", and named the five sites
136
- * that still emitted it, all reading `end_users.status === 'blocked'`. D1
137
- * landed. `users` has no `status` column, nothing reinstates one, and
138
- * removing a user's assignments is what withdraws access instead — so those
139
- * five sites went with the old tables.
140
- *
141
- * What is left in the cloud is a *shape* with no input: `TokenRefusalReason`
142
- * still admits `'blocked'` and `sendTokenRefusal` still has an arm for it
143
- * (`auth.ts`), as does `ws/realtime.ts` — but no site anywhere constructs
144
- * `reason: 'blocked'`, so neither arm is reachable. Verified by grep in
145
- * FL-007, after `routes.ts` listed this code on the dual-auth guard and a
146
- * review asked what produces it. Nothing does.
129
+ * **It has no producer today.** No user table carries a `status` column, and
130
+ * removing a user's assignments is what withdraws access instead. The cloud
131
+ * still has code paths shaped to carry this refusal, but nothing constructs
132
+ * one, so no caller can receive it.
147
133
  *
148
134
  * Kept, like `mcp_disabled` and for the same reason: the reserved shape is
149
135
  * the point, and a code removed from the vocabulary is a code the next
150
136
  * producer re-invents differently. But **do not list it as a refusal of any
151
137
  * route** — that would document an answer no caller can receive.
152
- *
153
- * The tense discipline this comment was written under still stands: it now
154
- * says the producer is gone because the producer is gone, not because a plan
155
- * expects it to be.
156
138
  */
157
139
  'account_blocked',
158
- // W4 — the command path.
159
- /** One job per action slug (§11.3); the refusal carries what is running. */
140
+ // The command path.
141
+ /** One job per action slug; the refusal carries what is running. */
160
142
  'busy',
161
- /** A parameter failed its §4.4 rule; details name the field and the rule. */
143
+ /** A parameter failed its declared rule; details name the field and the rule. */
162
144
  'parameter_invalid',
163
- /** The bridge could not account for this job after a restart (§6.1). */
145
+ /** The bridge could not account for this job after a restart. */
164
146
  'job_lost',
165
- /** Another user holds this publisher and has not been quiet long enough (§6.4). */
147
+ /** Another user holds this publisher and has not been quiet long enough. */
166
148
  'publisher_busy',
167
149
  /** A well-formed realtime frame this server does not know — the socket stays open. */
168
150
  'unknown_command',
@@ -173,8 +155,8 @@ export const ERROR_CODES = [
173
155
  * it sends them looking for a configuration mistake that is not there.
174
156
  */
175
157
  'not_subscribable',
176
- // W5 — cameras.
177
- /** The robot is connected but this camera is not publishing (§10). */
158
+ // Cameras.
159
+ /** The robot is connected but this camera is not publishing. */
178
160
  'camera_offline',
179
161
  /**
180
162
  * Nothing has been captured yet. An answer, not a failure: a camera
@@ -192,7 +174,7 @@ export const ERROR_CODES = [
192
174
  * nothing.
193
175
  */
194
176
  'wrong_kind',
195
- // W6 — retention and history.
177
+ // Retention and history.
196
178
  /**
197
179
  * The slug exists and is granted, but is configured live-only, so there is
198
180
  * no history to return. An empty array would be indistinguishable from a
@@ -209,7 +191,7 @@ export const ERROR_CODES = [
209
191
  */
210
192
  'not_aggregatable',
211
193
  /**
212
- * An org quota (§12.4) is exhausted. The message names **which** one —
194
+ * An org quota is exhausted. The message names **which** one —
213
195
  * "quota exceeded" without saying which is a dead end for whoever has to
214
196
  * act on it. Recording stops; live values keep flowing, because a storage
215
197
  * limit is not a reason to take a robot away from its operator.
@@ -226,13 +208,13 @@ export const ERROR_CODES = [
226
208
  'credential_in_use',
227
209
  /**
228
210
  * An action goal was never accepted — no server answered within the
229
- * bridge's patience (W5, from W4's review). Distinct from `failed`, which
211
+ * bridge's patience. Distinct from `failed`, which
230
212
  * means the robot tried: nothing tried here. It exists so a slug whose ROS
231
213
  * server is absent cannot stay wedged forever with the platform reporting
232
214
  * a machine as busy doing something it never started.
233
215
  */
234
216
  'goal_timeout',
235
- // W6a — deletion.
217
+ // Deletion.
236
218
  /**
237
219
  * A robot cannot be deleted while a live session is open. Refusing beats
238
220
  * deleting for the same reason `credential_in_use` does: the session
@@ -246,15 +228,15 @@ export const ERROR_CODES = [
246
228
  * A deletion destroyed some of a robot and then failed. The robot still
247
229
  * exists and is **not intact**; retrying the delete is the way out.
248
230
  *
249
- * It exists because the alternative was a generic `internal_error`, which
231
+ * It exists because the alternative is a generic `internal_error`, which
250
232
  * says "nothing happened" — and a caller who reads that goes looking for a
251
- * transient glitch. W6a's review measured the state it hides: configuration,
252
- * drafts, types and 300 000 rows gone, the robot still listed, and no audit
253
- * event. A failure that cannot be told apart from a no-op is how that state
254
- * stayed invisible.
233
+ * transient glitch. What it can hide is configuration, drafts, types and
234
+ * hundreds of thousands of rows already gone, the robot still listed, and no
235
+ * audit event. A failure that cannot be told apart from a no-op is how that
236
+ * state stays invisible.
255
237
  */
256
238
  'robot_deletion_partial',
257
- // W6b — addressing.
239
+ // Addressing.
258
240
  /**
259
241
  * The bridge will not queue another job: its queue is full.
260
242
  *
@@ -272,7 +254,7 @@ export const ERROR_CODES = [
272
254
  *
273
255
  * The numbers ride with it for the reason `publisher_busy` carries
274
256
  * `retry_after_ms`: a refusal that names a state and no action leaves the
275
- * caller to busy-loop, on a platform with no rate limiting until W8.
257
+ * caller to busy-loop.
276
258
  */
277
259
  'job_queue_full',
278
260
  /**
@@ -281,9 +263,9 @@ export const ERROR_CODES = [
281
263
  * **Path and query only.** A malformed uuid in a *body* is caught by the
282
264
  * body schema first and answers `validation_error` — the same mistake under
283
265
  * two codes, split by where the id sat. Stated here rather than promised
284
- * away: a consumer branching on `invalid_uuid` must not expect it for a
285
- * body field (Momus, W6b review). Unifying them is a W7 question, because
286
- * it means refusing before schema validation on every route that takes one.
266
+ * away: a consumer branching on `invalid_uuid` must not expect it for a body
267
+ * field. Unifying the two would mean refusing before schema validation on
268
+ * every route that takes an id.
287
269
  *
288
270
  * Distinct from `not_found`, which was the answer for both and made a
289
271
  * **typo indistinguishable from a deletion**. A developer whose client
@@ -292,7 +274,7 @@ export const ERROR_CODES = [
292
274
  * refused before any lookup — so it leaks nothing that `not_found` did not.
293
275
  */
294
276
  'invalid_uuid',
295
- // W6c — identity, and the limit that has to exist before it.
277
+ // Identity, and the limit that has to exist before it.
296
278
  /**
297
279
  * Too many attempts. The details carry `retry_after_ms`, for the reason
298
280
  * `publisher_busy` carries it: a refusal that names a state and no action
@@ -308,7 +290,7 @@ export const ERROR_CODES = [
308
290
  /**
309
291
  * The caller's **tier** is insufficient — an org Member reaching for what
310
292
  * only an Owner may do. Distinct from `forbidden`, which stays deliberately
311
- * silent about existence (§3.3): this one says nothing about the target
293
+ * silent about existence: this one says nothing about the target
312
294
  * either, only about the caller's own role, which they can already read.
313
295
  *
314
296
  * Without it, "ask an owner to do this" and "you have the wrong id" are the
@@ -322,7 +304,7 @@ export const ERROR_CODES = [
322
304
  * ask for a new link.
323
305
  */
324
306
  'token_spent',
325
- // W7 — the command path, still.
307
+ // The command path, continued.
326
308
  /**
327
309
  * A **service call** was dispatched and never returned. Distinct from
328
310
  * `goal_timeout`, which means an action goal was never *accepted* — nothing
@@ -331,13 +313,13 @@ export const ERROR_CODES = [
331
313
  * It exists because the bridge previously bounded a hung service with
332
314
  * nothing at all: `_invoke_service` took no patience, so the caller got
333
315
  * `bridge_timeout` from the cloud while the job stayed `running` forever on
334
- * both sides and the slug was busy for good (register row 2n, and 2e for the
335
- * cloud half). Rosie-W7 established that rclpy's
336
- * `Client.remove_pending_request` can abandon the future cheaply, so unlike
337
- * the action path this one can guarantee the callback never fires late.
316
+ * both sides and the slug busy for good. rclpy's
317
+ * `Client.remove_pending_request` abandons the pending future cheaply, so
318
+ * unlike the action path this one can guarantee the callback never fires
319
+ * late.
338
320
  */
339
321
  'service_timeout',
340
- // W7 — the asset store.
322
+ // The asset store.
341
323
  /**
342
324
  * A URDF references a mesh the store does not have. Distinct from
343
325
  * `not_found` on the URDF itself: the URDF is present and readable, and the
@@ -355,7 +337,7 @@ export const ERROR_CODES = [
355
337
  * cannot be read without the limit.
356
338
  */
357
339
  'asset_too_large',
358
- // W7b — the hosted authorization server.
340
+ // The hosted authorization server.
359
341
  //
360
342
  // **This comment was wrong in its first form and a teammate followed it
361
343
  // faithfully into a conformance bug.** It said these were "management-side
@@ -375,9 +357,8 @@ export const ERROR_CODES = [
375
357
  // (`/mcp/oauth/register` today), and as an ordinary `apiError` code at the
376
358
  // developer-facing management routes.
377
359
  //
378
- // Every code below has a producer landing in this same wave. W6b's lesson:
379
- // an enum value with no producer is precisely the defect that wave was
380
- // cataloguing, and a teammate was right to refuse to add one.
360
+ // Every code below has a producer. An enum value with no producer is a
361
+ // refusal a caller can never receive and a consumer must still branch on.
381
362
  /**
382
363
  * The app has not opted in to dynamic client registration. A normal app has
383
364
  * no reason to accept self-registering clients, so the flag is off by
@@ -398,7 +379,7 @@ export const ERROR_CODES = [
398
379
  * fix it.
399
380
  */
400
381
  'idp_unavailable',
401
- // W7c — the MCP server, and a THIRD dialect on the same process.
382
+ // The MCP server, and a THIRD dialect on the same process.
402
383
  //
403
384
  // The correction above is about two dialects; there are now three, and the
404
385
  // MCP endpoint speaks the one that is neither. **Inside the protocol** —
@@ -407,9 +388,7 @@ export const ERROR_CODES = [
407
388
  // general-purpose implementation of somebody else's specification, and a
408
389
  // body it cannot parse is indistinguishable from a broken server.
409
390
  //
410
- // **This claim was wider than the code in W7c's first version, and Momus-W7c
411
- // caught it in the same comment block whose opening sentence is about a
412
- // previous comment here misleading somebody.** The five refusals that happen
391
+ // **The claim is narrower than it first reads.** The five refusals that happen
413
392
  // *before* a bearer token is read — unknown app or MCP off (`404`), no or
414
393
  // bad token (`401`), foreign `Origin` (`403`), `GET`/`DELETE` (`405`),
415
394
  // malformed body (`400`) — are plain HTTP and answer `apiError`, exactly as
@@ -461,11 +440,11 @@ export const ERROR_CODES = [
461
440
  * **Deliberately one code for both**: to a developer holding the console,
462
441
  * the role's datasheet (`mcpRobotDatasheet`) already lists every exposure
463
442
  * the role does grant, so a second code would split an outcome nobody acts
464
- * on differently. To anyone else the two must be indistinguishable anyway —
465
- * §3.3.
443
+ * on differently. To anyone else the two must be indistinguishable anyway,
444
+ * because roles are the only filter.
466
445
  */
467
446
  'tool_not_available',
468
- // W9 — capabilities.
447
+ // Capabilities.
469
448
  /**
470
449
  * An app-wide **capability** the caller's role does not grant — today
471
450
  * `assets` (`GET /api/robots/:id/assets` and the URDF/by-id byte routes)
@@ -475,16 +454,15 @@ export const ERROR_CODES = [
475
454
  * **Distinct from `forbidden`, and the distinction is the point.**
476
455
  * `forbidden` is deliberately silent about existence, because roles are the
477
456
  * only filter and a slug the caller cannot use must be indistinguishable
478
- * from a slug that is not there (§3.3). A capability is not a slug: it is a
457
+ * from a slug that is not there. A capability is not a slug: it is a
479
458
  * switch in the console that the developer owns, and the caller reaching
480
459
  * this refusal has already been proven to reach the robot. Answering
481
460
  * `forbidden` there tells a developer only that they may not — not which
482
461
  * toggle to flip — and a promise the console makes is exactly what these
483
462
  * capabilities have historically failed to keep.
484
463
  *
485
- * Both gates answered differently for one wave: `assets` said `forbidden`,
486
- * the newer `action_history` said this. One decision with two codes makes a
487
- * client branch on which route it called, so `assets` was moved here.
464
+ * Both capability gates answer with this code. One decision with two codes
465
+ * would make a client branch on which route it called.
488
466
  */
489
467
  'capability_required',
490
468
  // 2026-08-29 — org-central identity (D1/D2).
@@ -493,13 +471,9 @@ export const ERROR_CODES = [
493
471
  * deletable nor demotable. 409, on both `DELETE /api/org/users/:id` and
494
472
  * `PATCH /api/org/users/:id/tier`.
495
473
  *
496
- * **It was already being emitted before it was registered here** — the cloud
497
- * has answered `last_owner` from `org-members.ts` since W3a, and
498
- * `identity.ts`'s own doc comment named it, but `sendError` takes a bare
499
- * `string` and nothing ever compared the two lists. So a consumer switching
500
- * exhaustively over `ERROR_CODES` could not handle a code the server
501
- * actually sends. Registered as part of carrying the rule onto the new
502
- * tiers, and named as the pre-existing gap it was rather than as a new code.
474
+ * A code the server sends must be in this list, or a consumer switching
475
+ * exhaustively over `ERROR_CODES` cannot handle it. Nothing compares the two
476
+ * automatically, because the cloud's error helper takes a bare string.
503
477
  *
504
478
  * Deliberately not `forbidden` or `tier_required`: an Owner reaching this
505
479
  * has every permission the act needs. The refusal is about the org's
@@ -549,7 +523,7 @@ export const ERROR_CODES = [
549
523
  * `routes/console-oauth.ts` in the same release.
550
524
  */
551
525
  'signup_closed',
552
- // FL-007 (route manifest): emitted by the cloud, catalogued late.
526
+ // Emitted by the cloud, catalogued late.
553
527
  //
554
528
  // Every one of the five below has had a live producer for some time; what
555
529
  // they never had was an entry here. **Each was confirmed by grepping the
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: Apache-2.0
1
2
  import { z } from 'zod';
2
3
  /**
3
4
  * **Fleetless users: the org's team, and the only people who reach the
@@ -128,7 +129,7 @@ export declare const fleetlessUserListResponse: z.ZodObject<{
128
129
  }, z.core.$strip>;
129
130
  export type FleetlessUserListResponse = z.infer<typeof fleetlessUserListResponse>;
130
131
  /**
131
- * Access plus refresh (spec §3.4). The access token is short-lived; the
132
+ * Access plus refresh. The access token is short-lived; the
132
133
  * refresh token rotates on every use, so a stolen one is detectable when the
133
134
  * original is presented again.
134
135
  *
@@ -146,8 +147,8 @@ export declare const refreshRequest: z.ZodObject<{
146
147
  }, z.core.$strip>;
147
148
  export type RefreshRequest = z.infer<typeof refreshRequest>;
148
149
  /**
149
- * Registering an org creates the org and its first owner in one step
150
- * (André, 2026-08-10): whoever registers the organisation is the owner.
150
+ * Registering an org creates the org and its first owner in one step: whoever
151
+ * registers the organisation is the owner.
151
152
  */
152
153
  export declare const signUpRequest: z.ZodObject<{
153
154
  org_name: z.ZodString;
@@ -222,7 +223,7 @@ export declare const developerLoginRequest: z.ZodObject<{
222
223
  }, z.core.$strip>;
223
224
  export type DeveloperLoginRequest = z.infer<typeof developerLoginRequest>;
224
225
  /**
225
- * What happened to the mail, in four words instead of one (W6c).
226
+ * What happened to the mail, in four words instead of one.
226
227
  *
227
228
  * `mail_sent: boolean` could not tell **"we have no SMTP configured"** from
228
229
  * **"we tried and the server refused"**, so the console had to pick a sentence
@@ -387,10 +388,10 @@ export declare const tierChangeRequest: z.ZodObject<{
387
388
  export type TierChangeRequest = z.infer<typeof tierChangeRequest>;
388
389
  /**
389
390
  * What a `forbidden` refusal carries when the reason is the caller's **tier**
390
- * rather than a missing grant (W6c).
391
+ * rather than a missing grant.
391
392
  *
392
- * §3.3 makes `forbidden` deliberately silent about *existence*, and that stays
393
- * true — this says nothing about what the target is. But "your role does not
393
+ * `forbidden` is deliberately silent about *existence*, and that stays true —
394
+ * this says nothing about what the target is. But "your role does not
394
395
  * permit this" and "there is no such thing" are the same answer today, and a
395
396
  * developer cannot tell *ask an owner* from *you have the wrong id*. Naming
396
397
  * the required tier reveals only what the caller could read off the docs.
@@ -429,8 +430,8 @@ export type PasswordChangeRequest = z.infer<typeof passwordChangeRequest>;
429
430
  *
430
431
  * **The response never says whether the address exists.** It is unauthenticated
431
432
  * and would otherwise be an account-enumeration oracle — the one place where
432
- * §3.3's "reveal nothing about what exists" is not a preference but the whole
433
- * point. So this answers the same way for a known and an unknown address, in
433
+ * revealing nothing about what exists is not a preference but the whole point.
434
+ * So this answers the same way for a known and an unknown address, in
434
435
  * status, body **and timing**, and any consumer that renders "no such account"
435
436
  * from it has reintroduced the oracle.
436
437
  *
@@ -463,24 +464,20 @@ export type PasswordResetConfirm = z.infer<typeof passwordResetConfirm>;
463
464
  * An IdP issuer URL — **an attacker-supplied string that decides where the
464
465
  * *server* connects.**
465
466
  *
466
- * `redirectUri` in `oauth.ts` got a parsed scheme check and an explicit
467
- * loopback allow-list, with the reasoning written down, because it decides
468
- * where a *credential* goes. This field got `z.url()` — in the same file, in
469
- * the same wave. Argus-W7b found it and stored `file:///etc/passwd`,
470
- * `http://169.254.169.254/latest/meta-data` and `http://infra-postgres-1:5432`
471
- * through the app's IdP route, then caught the outbound discovery fetch on a
472
- * listener he stood up. **That is this project's own question — which rules
473
- * have we already written down, and where else do they apply — answered badly,
474
- * one field over.**
467
+ * `redirectUri` in `oauth.ts` carries a parsed scheme check and an explicit
468
+ * loopback allow-list because it decides where a *credential* goes. A bare
469
+ * `z.url()` here would accept `file:///etc/passwd`, a cloud metadata address or
470
+ * an internal database host, and the server would then fetch it during issuer
471
+ * discovery. The same rule applies one field over.
475
472
  *
476
473
  * **What this shape can decide, it now decides:** http(s) only (so no `file:`,
477
474
  * `gopher:`, `data:`), no credentials in the URL, no fragment, no query. RFC
478
- * 8414 §3 builds the discovery URL from the issuer's path, so a query string
475
+ * 8414 builds the discovery URL from the issuer's path, so a query string
479
476
  * there is meaningless and a `@` is a redirect trick.
480
477
  *
481
478
  * **What it cannot decide, stated rather than implied:** it cannot tell
482
- * `http://localhost:8081/realms/fleetless-test` — the dev IdP this project
483
- * ships — from `http://127.0.0.1:5432`. Both are loopback http. So **this is
479
+ * a development identity provider on `http://localhost:8081` from a database
480
+ * on `http://127.0.0.1:5432`. Both are loopback http. So **this is
484
481
  * not the SSRF defence and must not be mistaken for one.** The defence belongs
485
482
  * at the fetch, in the cloud: refuse loopback, link-local and private ranges
486
483
  * unless something explicitly opts in for development, and it names DNS
package/dist/identity.js CHANGED
@@ -126,7 +126,7 @@ export const fleetlessUserListResponse = z.object({
126
126
  }),
127
127
  });
128
128
  /**
129
- * Access plus refresh (spec §3.4). The access token is short-lived; the
129
+ * Access plus refresh. The access token is short-lived; the
130
130
  * refresh token rotates on every use, so a stolen one is detectable when the
131
131
  * original is presented again.
132
132
  *
@@ -146,8 +146,8 @@ export const sessionTokens = z.object({
146
146
  });
147
147
  export const refreshRequest = z.object({ refresh_token: z.string().min(1) });
148
148
  /**
149
- * Registering an org creates the org and its first owner in one step
150
- * (André, 2026-08-10): whoever registers the organisation is the owner.
149
+ * Registering an org creates the org and its first owner in one step: whoever
150
+ * registers the organisation is the owner.
151
151
  */
152
152
  export const signUpRequest = z.object({
153
153
  org_name: z.string().min(1).max(120),
@@ -198,7 +198,7 @@ export const developerLoginRequest = z.object({
198
198
  password: z.string().min(1),
199
199
  });
200
200
  /**
201
- * What happened to the mail, in four words instead of one (W6c).
201
+ * What happened to the mail, in four words instead of one.
202
202
  *
203
203
  * `mail_sent: boolean` could not tell **"we have no SMTP configured"** from
204
204
  * **"we tried and the server refused"**, so the console had to pick a sentence
@@ -353,10 +353,10 @@ export const patchFleetlessUserRequest = z
353
353
  export const tierChangeRequest = z.object({ tier: orgAdminTier }).strict();
354
354
  /**
355
355
  * What a `forbidden` refusal carries when the reason is the caller's **tier**
356
- * rather than a missing grant (W6c).
356
+ * rather than a missing grant.
357
357
  *
358
- * §3.3 makes `forbidden` deliberately silent about *existence*, and that stays
359
- * true — this says nothing about what the target is. But "your role does not
358
+ * `forbidden` is deliberately silent about *existence*, and that stays true —
359
+ * this says nothing about what the target is. But "your role does not
360
360
  * permit this" and "there is no such thing" are the same answer today, and a
361
361
  * developer cannot tell *ask an owner* from *you have the wrong id*. Naming
362
362
  * the required tier reveals only what the caller could read off the docs.
@@ -392,8 +392,8 @@ export const passwordChangeRequest = z.object({
392
392
  *
393
393
  * **The response never says whether the address exists.** It is unauthenticated
394
394
  * and would otherwise be an account-enumeration oracle — the one place where
395
- * §3.3's "reveal nothing about what exists" is not a preference but the whole
396
- * point. So this answers the same way for a known and an unknown address, in
395
+ * revealing nothing about what exists is not a preference but the whole point.
396
+ * So this answers the same way for a known and an unknown address, in
397
397
  * status, body **and timing**, and any consumer that renders "no such account"
398
398
  * from it has reintroduced the oracle.
399
399
  *
@@ -424,24 +424,20 @@ export const passwordResetConfirm = z.object({
424
424
  * An IdP issuer URL — **an attacker-supplied string that decides where the
425
425
  * *server* connects.**
426
426
  *
427
- * `redirectUri` in `oauth.ts` got a parsed scheme check and an explicit
428
- * loopback allow-list, with the reasoning written down, because it decides
429
- * where a *credential* goes. This field got `z.url()` — in the same file, in
430
- * the same wave. Argus-W7b found it and stored `file:///etc/passwd`,
431
- * `http://169.254.169.254/latest/meta-data` and `http://infra-postgres-1:5432`
432
- * through the app's IdP route, then caught the outbound discovery fetch on a
433
- * listener he stood up. **That is this project's own question — which rules
434
- * have we already written down, and where else do they apply — answered badly,
435
- * one field over.**
427
+ * `redirectUri` in `oauth.ts` carries a parsed scheme check and an explicit
428
+ * loopback allow-list because it decides where a *credential* goes. A bare
429
+ * `z.url()` here would accept `file:///etc/passwd`, a cloud metadata address or
430
+ * an internal database host, and the server would then fetch it during issuer
431
+ * discovery. The same rule applies one field over.
436
432
  *
437
433
  * **What this shape can decide, it now decides:** http(s) only (so no `file:`,
438
434
  * `gopher:`, `data:`), no credentials in the URL, no fragment, no query. RFC
439
- * 8414 §3 builds the discovery URL from the issuer's path, so a query string
435
+ * 8414 builds the discovery URL from the issuer's path, so a query string
440
436
  * there is meaningless and a `@` is a redirect trick.
441
437
  *
442
438
  * **What it cannot decide, stated rather than implied:** it cannot tell
443
- * `http://localhost:8081/realms/fleetless-test` — the dev IdP this project
444
- * ships — from `http://127.0.0.1:5432`. Both are loopback http. So **this is
439
+ * a development identity provider on `http://localhost:8081` from a database
440
+ * on `http://127.0.0.1:5432`. Both are loopback http. So **this is
445
441
  * not the SSRF defence and must not be mistaken for one.** The defence belongs
446
442
  * at the fetch, in the cloud: refuse loopback, link-local and private ranges
447
443
  * unless something explicitly opts in for development, and it names DNS
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: Apache-2.0
1
2
  export { SLUG_RULE, ROS_NAME_RULE, ROS_TYPE_NAME_RULE, FIELD_PATH_RULE, slug, rosName, rosTypeName, fieldPath, wireSeqCursor, wireTimestampMs, applyErrorKind, applyError, } from './common.js';
2
3
  export type { ApplyErrorKind, ApplyError } from './common.js';
3
4
  export { MCP_PROTOCOL_VERSION, MCP_ENDPOINT_PATH, mcpAppEndpointPath, MCP_APP_PATHS, MCP_TOOL_NAME_MAX, MCP_ASSET_LINK_PATH, MCP_ASSET_LINK_TTL_MS, mcpToolNamePattern, mcpToolKind, mcpExposure, mcpCapabilities, mcpRobotDatasheet, mcpRolePreviewResponse, } from './mcp.js';
@@ -12,10 +13,9 @@ export { FLEETLESS_FORMAT_VERSION, RESERVED_SLUGS, parameterType, parameterSpec,
12
13
  export type { CameraSource } from './config.js';
13
14
  export type { ParameterType, ParameterSpec, ActionConfig, ServiceConfig, PublisherConfig, CameraConfig, CameraCredentials, AlertCondition, DatapointAlert, DatapointNumeric, DatapointRetention, DatapointChart, DatapointConfig, RobotConfigDoc, ValidationIssue, ConfigState, } from './config.js';
14
15
  /**
15
- * FL-005 — the one account of what is wrong with a document, shared by the
16
- * cloud and the console. The cloud held a second copy for one wave; it was
17
- * deleted in wave 2 task 8 (cloud `a307e18`, 2026-09-03). See
18
- * `config-issues.ts`'s header for what that window cost.
16
+ * The one account of what is wrong with a configuration document, shared by
17
+ * every layer that reports on one. See `config-issues.ts`'s header for why a
18
+ * second copy of this vocabulary is a defect rather than a convenience.
19
19
  */
20
20
  export { DOCUMENT_ROOT_PATH, EXPOSURE_SECTIONS, schemaIssues, formatPath, splitFormatPath, configSchemaHash, } from './config-issues.js';
21
21
  export type { SchemaIssue, ExposureSection } from './config-issues.js';
package/dist/index.js CHANGED
@@ -2,31 +2,30 @@
2
2
  export { SLUG_RULE, ROS_NAME_RULE, ROS_TYPE_NAME_RULE, FIELD_PATH_RULE, slug, rosName, rosTypeName, fieldPath, wireSeqCursor, wireTimestampMs, applyErrorKind, applyError, } from './common.js';
3
3
  export { MCP_PROTOCOL_VERSION, MCP_ENDPOINT_PATH, mcpAppEndpointPath, MCP_APP_PATHS, MCP_TOOL_NAME_MAX, MCP_ASSET_LINK_PATH, MCP_ASSET_LINK_TTL_MS, mcpToolNamePattern, mcpToolKind, mcpExposure, mcpCapabilities, mcpRobotDatasheet, mcpRolePreviewResponse, } from './mcp.js';
4
4
  export { PROTOCOL_VERSION, bridgeHello, cloudHelloOk, cloudHelloError, cloudPing, bridgePong, datapointFrame, bridgeState, bridgePressure, PRESSURE_SLUG, cloudConfig, bridgeConfigApplied, cloudIntrospectRequest, bridgeIntrospect, cloudTypeRequest, bridgeTypeDefinitions, cloudInvoke, cloudCancel, cloudPublish, bridgeJobUpdate, bridgeJobLost, snapshotHeader, cloudCameraStart, cloudCameraStop, bridgeCameraState, SNAPSHOT_MAX_BYTES, CLOSE_ROBOT_DELETED,
5
- // W7 — assets.
5
+ // Assets.
6
6
  bridgeAssetsAvailable, cloudAssetRequest, bridgeAssetProgress,
7
- // W6b — addressing.
7
+ // Addressing.
8
8
  activeJob, DEFAULT_PATIENCE_MS, MAX_PATIENCE_MS, MIN_PATIENCE_MS, } from './protocol.js';
9
9
  export { jobState, job, jobEvent, busyDetails, publisherBusyDetails, jobQueueFullDetails } from './jobs.js';
10
10
  export { JOB_RUN_PAGE_MAX, JOB_RUN_RETENTION_DAYS, jobActor, jobRunKind, jobRun, jobRunQuery, jobRunListResponse, jobRunSummaryQuery, jobRunSummary, } from './jobs.js';
11
11
  export { FLEETLESS_FORMAT_VERSION, RESERVED_SLUGS, parameterType, parameterSpec, parameterMap, serviceDescription, parameterDescription, messageTemplate, messageRef, messageBody, messageMap, PLACEHOLDER_RE, placeholderNames, actionConfig, serviceConfig, publisherConfig, cameraConfig, alertCondition, datapointAlert, rateThrottleHz, datapointNumeric, datapointRetention, datapointChart, datapointConfig, robotConfigDoc, validationIssue, configState, snapshotIntervalSeconds,
12
- // FL-002 — the defaults the format names, so nobody invents them twice.
12
+ // The defaults the format names, so nobody invents them twice.
13
13
  ALERT_SEVERITY_DEFAULT, ALERT_ENABLED_DEFAULT, RETENTION_INTERVAL_SECONDS_DEFAULT, CHART_WINDOW_MINUTES_DEFAULT,
14
- // W6
14
+ // Retention, history and quotas.
15
15
  cameraSource, cameraCredentials, } from './config.js';
16
16
  /**
17
- * FL-005 — the one account of what is wrong with a document, shared by the
18
- * cloud and the console. The cloud held a second copy for one wave; it was
19
- * deleted in wave 2 task 8 (cloud `a307e18`, 2026-09-03). See
20
- * `config-issues.ts`'s header for what that window cost.
17
+ * The one account of what is wrong with a configuration document, shared by
18
+ * every layer that reports on one. See `config-issues.ts`'s header for why a
19
+ * second copy of this vocabulary is a defect rather than a convenience.
21
20
  */
22
21
  export { DOCUMENT_ROOT_PATH, EXPOSURE_SECTIONS, schemaIssues, formatPath, splitFormatPath, configSchemaHash, } from './config-issues.js';
23
22
  export { rosGraphEntry, rosGraph, typeField, typeDefinition, parameterFieldsOf } from './introspection.js';
24
23
  export { robot, patchRobotResponse, robotToken, createRobotRequest, createRobotResponse, exposureCounts, robotListItem, robotListResponse, datapointValue, robotDetailResponse, configDraftResponse, putConfigDraftRequest, publishConfigResponse, configVersionsResponse, configVersionResponse, introspectionResponse, typesResponse, fetchTypesRequest, fetchTypesResponse, datapointDescriptor, datapointListResponse, robotDetailsDoc, putRobotDetailsResponse, putRobotDetailsRequest, invokeRequest, invokeResponse,
25
- // W6b — addressing.
24
+ // Addressing.
26
25
  cancelRequest, releaseLiveQuery, serviceCallResponse, invokeOrServiceResponse, publishRequest, jobResponse, robotJobsResponse, rateLimitDetails, exposure, exposureListResponse, SNAPSHOT_HEADERS, ASSET_UPLOAD_HEADERS, cameraDescriptor, cameraListResponse, liveSessionResponse, snapshotMetaResponse,
27
- // W6 — retention, history, quotas.
26
+ // Retention, history, quotas.
28
27
  historyQuery, historySamplesResponse, historyBucketsResponse, historyResponse, orgQuotas, orgQuotaUsage, orgQuotaUsageCounts, robotDeletionSummary, RESOURCE_HEALTH_STATES, resourceHealthState, resourceHealthListResponse, orgHealthQuery, robotDeleteQuery,
29
- // W3a — robot rename, slug rename.
28
+ // Robot rename, slug rename.
30
29
  patchRobotRequest, renameSlugRequest, renameSlugResponse, slugUsageResponse, } from './rest.js';
31
30
  export { LATENCY_BUCKET_MS, BRIDGE_LATENCY_RETENTION_DAYS, MAX_LATENCY_BUCKETS_PER_RESPONSE, latencyBucket, robotLatencySeries, orgLatencyQuery, orgLatencyResponse, USAGE_WINDOW_MAX_DAYS, usageMetric, usageDay, orgUsageQuery, usageRow, orgUsageResponse, } from './rest.js';
32
31
  export { clientAuth, authOk, authError, clientInvoke, clientCancel, clientPublish, commandResult, errorFrame, clientSubscribe, clientUnsubscribe, subscribeError, datapointEvent, resourceHealthEvent, resourceHealthCleared, liveSessionEndReason, liveSessionEvent, ORG_EVENT_SAMPLE_INTERVAL_MS, ORG_EVENT_ORG_CEILING_PER_SECOND, ORG_EVENT_BUFFER_SIZE, ORG_EVENT_BUFFER_IDLE_MS, ORG_EVENT_DETAIL_MAX_BYTES, orgEventKind, orgEventSeverity, orgEvent, orgEventSubscribe, orgEventUnsubscribe, orgEventReplay, orgEventDropped, } from './realtime.js';
@@ -35,9 +34,9 @@ export { password, org, patchOrgResponse, sessionTokens, refreshRequest, signUpR
35
34
  waitlistRequest, developerLoginRequest,
36
35
  // 2026-09-05 — the two identity spaces (app-user-auth, D1).
37
36
  USER_DISPLAY_NAME_MAX, orgAdminTier, fleetlessUser, fleetlessUserListResponse, createTeamInviteRequest, teamInvite, pendingTeamInvite, pendingTeamInviteListResponse, acceptTeamInviteRequest, patchFleetlessUserRequest, tierChangeRequest,
38
- // W6c — identity.
37
+ // Identity.
39
38
  mailStatus, tierRequiredDetails, passwordChangeRequest, passwordResetRequest, passwordResetConfirm, idpIssuer,
40
- // W3a — auth/me, org and member patches.
39
+ // auth/me, org and member patches.
41
40
  authMeResponse, patchOrgRequest, patchAuthMeRequest, } from './identity.js';
42
41
  export { appIdentifier, app, appListResponse, createAppRequest, updateAppRequest, serverKeyToken, serverKey, serverKeyListResponse, createServerKeyResponse, role, roleListResponse, rolePermissions, } from './apps.js';
43
42
  export { clientLoginRequest, clientRefreshRequest, clientLogoutRequest, clientRegisterRequest, clientVerifyEmailRequest, clientResendVerificationRequest, clientPasswordResetRequest, clientPasswordResetConfirmRequest, clientAcceptInvitationRequest, CLIENT_OIDC_CALLBACK_PATH, clientProviderListQuery, clientProviderListResponse, clientOidcStartQuery, clientOidcCallbackQuery, clientOidcExchangeRequest, clientOidcErrorCode, clientMcpInteraction, clientMcpInteractionDecisionResponse, mcpConsentGrant, mcpConsentGrantListResponse, clientIdentity, } from './client-auth.js';