@fleetless/contracts 1.0.5 → 1.1.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.
Files changed (96) hide show
  1. package/CHANGELOG.md +17 -2
  2. package/CONTRIBUTING.md +100 -75
  3. package/README.md +69 -83
  4. package/SECURITY.md +24 -24
  5. package/artifacts/openapi.json +359 -65
  6. package/artifacts/routes.json +74 -3
  7. package/artifacts/schema/app-list-response.schema.json +2 -2
  8. package/artifacts/schema/app-oidc-provider-list-response.schema.json +1 -1
  9. package/artifacts/schema/app-oidc-provider.schema.json +1 -1
  10. package/artifacts/schema/app-user-list-response.schema.json +2 -2
  11. package/artifacts/schema/app-user.schema.json +2 -2
  12. package/artifacts/schema/app.schema.json +2 -2
  13. package/artifacts/schema/asset-list-response.schema.json +7 -7
  14. package/artifacts/schema/asset-sync-request.schema.json +1 -1
  15. package/artifacts/schema/asset-sync-status.schema.json +1 -1
  16. package/artifacts/schema/asset.schema.json +3 -3
  17. package/artifacts/schema/auth-me-response.schema.json +2 -2
  18. package/artifacts/schema/auth-ok.schema.json +1 -1
  19. package/artifacts/schema/authorization-server-metadata.schema.json +1 -1
  20. package/artifacts/schema/bridge-asset-progress.schema.json +1 -1
  21. package/artifacts/schema/busy-details.schema.json +3 -3
  22. package/artifacts/schema/client-identity.schema.json +1 -1
  23. package/artifacts/schema/client-login-request.schema.json +1 -1
  24. package/artifacts/schema/client-logout-request.schema.json +1 -1
  25. package/artifacts/schema/client-mcp-interaction.schema.json +1 -1
  26. package/artifacts/schema/client-robot-list-item.schema.json +70 -0
  27. package/artifacts/schema/client-robot-list-response.schema.json +83 -0
  28. package/artifacts/schema/cloud-config.schema.json +1 -1
  29. package/artifacts/schema/command-result.schema.json +3 -3
  30. package/artifacts/schema/config-draft-response.schema.json +1 -1
  31. package/artifacts/schema/config-version-response.schema.json +1 -1
  32. package/artifacts/schema/create-server-key-response.schema.json +1 -1
  33. package/artifacts/schema/datapoint-config.schema.json +1 -1
  34. package/artifacts/schema/datapoint-value.schema.json +2 -2
  35. package/artifacts/schema/dynamic-client-registration-request.schema.json +2 -2
  36. package/artifacts/schema/fleetless-user-list-response.schema.json +2 -2
  37. package/artifacts/schema/fleetless-user.schema.json +2 -2
  38. package/artifacts/schema/invoke-or-service-response.schema.json +4 -4
  39. package/artifacts/schema/invoke-response.schema.json +3 -3
  40. package/artifacts/schema/job-actor.schema.json +1 -1
  41. package/artifacts/schema/job-event.schema.json +3 -3
  42. package/artifacts/schema/job-response.schema.json +3 -3
  43. package/artifacts/schema/job-run-list-response.schema.json +2 -2
  44. package/artifacts/schema/job-run.schema.json +2 -2
  45. package/artifacts/schema/job.schema.json +3 -3
  46. package/artifacts/schema/mcp-consent-grant-list-response.schema.json +2 -2
  47. package/artifacts/schema/mcp-consent-grant.schema.json +2 -2
  48. package/artifacts/schema/oauth-authorize-query.schema.json +1 -1
  49. package/artifacts/schema/oauth-token-request.schema.json +1 -1
  50. package/artifacts/schema/patch-org-response.schema.json +1 -1
  51. package/artifacts/schema/patch-robot-response.schema.json +1 -1
  52. package/artifacts/schema/robot-config-doc.schema.json +1 -1
  53. package/artifacts/schema/robot-jobs-response.schema.json +3 -3
  54. package/artifacts/schema/role-list-response.schema.json +1 -1
  55. package/artifacts/schema/role.schema.json +1 -1
  56. package/artifacts/schema/server-key-list-response.schema.json +2 -2
  57. package/artifacts/schema/server-key.schema.json +1 -1
  58. package/artifacts/schema/service-call-response.schema.json +1 -1
  59. package/artifacts/schema/sign-up-response.schema.json +2 -2
  60. package/artifacts/schema/urdf-completeness.schema.json +2 -2
  61. package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +1 -1
  62. package/dist/alerts.d.ts +15 -17
  63. package/dist/alerts.js +15 -17
  64. package/dist/app-users.d.ts +5 -6
  65. package/dist/app-users.js +9 -10
  66. package/dist/apps.d.ts +5 -6
  67. package/dist/apps.js +11 -12
  68. package/dist/assets.js +10 -13
  69. package/dist/audit.d.ts +10 -12
  70. package/dist/audit.js +14 -17
  71. package/dist/client-auth.d.ts +8 -9
  72. package/dist/client-auth.js +14 -15
  73. package/dist/client-robots.d.ts +37 -0
  74. package/dist/client-robots.js +30 -0
  75. package/dist/config-issues.d.ts +3 -3
  76. package/dist/config-issues.js +3 -3
  77. package/dist/config.d.ts +5 -6
  78. package/dist/config.js +7 -8
  79. package/dist/errors.d.ts +4 -4
  80. package/dist/errors.js +8 -9
  81. package/dist/identity.d.ts +4 -5
  82. package/dist/identity.js +7 -8
  83. package/dist/index.d.ts +3 -1
  84. package/dist/index.js +2 -1
  85. package/dist/jobs.js +5 -5
  86. package/dist/mcp.d.ts +11 -9
  87. package/dist/mcp.js +8 -3
  88. package/dist/oauth.d.ts +8 -10
  89. package/dist/oauth.js +13 -15
  90. package/dist/protocol.d.ts +7 -8
  91. package/dist/protocol.js +20 -22
  92. package/dist/realtime.d.ts +2 -2
  93. package/dist/realtime.js +4 -4
  94. package/dist/rest.js +4 -4
  95. package/dist/routes.js +40 -4
  96. package/package.json +1 -1
package/dist/alerts.d.ts CHANGED
@@ -1,24 +1,22 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  import { z } from 'zod';
3
- /**
4
3
  /**
5
4
  * Datapoint alerts and per-datapoint chart display config.
6
5
  *
7
- * An alert is a **state machine** (`ok ⇄ firing`), not a fire-once event. The
6
+ * An alert is a **state machine** (`ok ⇄ firing`), not a fire-once event: the
8
7
  * definition and the runtime state (`state`, `state_since`, `last_value`) are
9
- * read together, and the cloud evaluates the alert at ingest.
8
+ * read together, and the cloud evaluates it at ingest.
10
9
  *
11
10
  * **The definitions live in the configuration document.** The alert definition
12
11
  * is `config.ts`'s `datapointAlert`, nested under the datapoint it watches; the
13
- * chart bounds are `datapointChart`. They therefore take effect on publish
14
- * rather than immediately, and in exchange every change to them is versioned,
15
- * comparable and revertible. The runtime state stays in the database: it has no
16
- * business in a versioned document.
12
+ * chart bounds are `datapointChart`. They take effect on publish, not
13
+ * immediately — in exchange, every change is versioned, comparable, revertible.
14
+ * The runtime state stays in the database: it has no business in a versioned
15
+ * document.
17
16
  *
18
17
  * **What is here is the read surface.** `GET /api/robots/:id/alerts` and
19
- * `GET /api/org/alerts` answer with the definition joined to its state, and the
20
- * shapes below are what they answer with. No alert sends mail, so nothing here
21
- * describes one.
18
+ * `GET /api/org/alerts` answer with the definition joined to its state — the
19
+ * shapes below. No alert sends mail, so nothing here describes one.
22
20
  */
23
21
  /**
24
22
  * `above`/`below` compare the numeric sample value (already scale/offset
@@ -27,11 +25,11 @@ import { z } from 'zod';
27
25
  * threshold itself: `above` resolves at `value ≤ threshold −
28
26
  * resolve_hysteresis`, mirrored for `below`. Defaulted rather than left
29
27
  * `optional` so a parsed entity never makes a consumer re-derive "absent
30
- * means 0" — the cloud always sends an explicit resolve point, and every
31
- * reader gets the same number whether it was sent or not. `equals` compares
32
- * the raw value for equality — the shape for boolean/string datapoints a
33
- * threshold cannot describe ("Hindernis erkannt" = `equals true`) — and
34
- * carries no hysteresis, because equality has no direction to relax.
28
+ * means 0" — the cloud always sends an explicit resolve point. `equals`
29
+ * compares the raw value for equality — the shape for boolean/string
30
+ * datapoints a threshold cannot describe (an obstacle-detected flag as
31
+ * `equals true`) — and carries no hysteresis: equality has no direction to
32
+ * relax.
35
33
  *
36
34
  * A discriminated union on `kind` rather than one object with optional
37
35
  * fields: an `equals` alert carrying a stray `threshold` would otherwise
@@ -101,8 +99,8 @@ export type AlertState = z.infer<typeof alertState>;
101
99
  * cloud joins the two per request (`routes/alerts.ts`'s `toWire`).
102
100
  *
103
101
  * **There are no mail settings here.** The configuration format has no mail
104
- * fields, so no alert can be configured to send one, and a shape describing
105
- * recipients would describe a delivery path that does not exist.
102
+ * fields, so no alert can send one, and a shape describing recipients would
103
+ * describe a delivery path that does not exist.
106
104
  */
107
105
  export declare const datapointAlertRow: z.ZodObject<{
108
106
  id: z.ZodUUID;
package/dist/alerts.js CHANGED
@@ -1,25 +1,23 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  import { z } from 'zod';
3
3
  import { slug } from './common.js';
4
- /**
5
4
  /**
6
5
  * Datapoint alerts and per-datapoint chart display config.
7
6
  *
8
- * An alert is a **state machine** (`ok ⇄ firing`), not a fire-once event. The
7
+ * An alert is a **state machine** (`ok ⇄ firing`), not a fire-once event: the
9
8
  * definition and the runtime state (`state`, `state_since`, `last_value`) are
10
- * read together, and the cloud evaluates the alert at ingest.
9
+ * read together, and the cloud evaluates it at ingest.
11
10
  *
12
11
  * **The definitions live in the configuration document.** The alert definition
13
12
  * is `config.ts`'s `datapointAlert`, nested under the datapoint it watches; the
14
- * chart bounds are `datapointChart`. They therefore take effect on publish
15
- * rather than immediately, and in exchange every change to them is versioned,
16
- * comparable and revertible. The runtime state stays in the database: it has no
17
- * business in a versioned document.
13
+ * chart bounds are `datapointChart`. They take effect on publish, not
14
+ * immediately — in exchange, every change is versioned, comparable, revertible.
15
+ * The runtime state stays in the database: it has no business in a versioned
16
+ * document.
18
17
  *
19
18
  * **What is here is the read surface.** `GET /api/robots/:id/alerts` and
20
- * `GET /api/org/alerts` answer with the definition joined to its state, and the
21
- * shapes below are what they answer with. No alert sends mail, so nothing here
22
- * describes one.
19
+ * `GET /api/org/alerts` answer with the definition joined to its state — the
20
+ * shapes below. No alert sends mail, so nothing here describes one.
23
21
  */
24
22
  /**
25
23
  * `above`/`below` compare the numeric sample value (already scale/offset
@@ -28,11 +26,11 @@ import { slug } from './common.js';
28
26
  * threshold itself: `above` resolves at `value ≤ threshold −
29
27
  * resolve_hysteresis`, mirrored for `below`. Defaulted rather than left
30
28
  * `optional` so a parsed entity never makes a consumer re-derive "absent
31
- * means 0" — the cloud always sends an explicit resolve point, and every
32
- * reader gets the same number whether it was sent or not. `equals` compares
33
- * the raw value for equality — the shape for boolean/string datapoints a
34
- * threshold cannot describe ("Hindernis erkannt" = `equals true`) — and
35
- * carries no hysteresis, because equality has no direction to relax.
29
+ * means 0" — the cloud always sends an explicit resolve point. `equals`
30
+ * compares the raw value for equality — the shape for boolean/string
31
+ * datapoints a threshold cannot describe (an obstacle-detected flag as
32
+ * `equals true`) — and carries no hysteresis: equality has no direction to
33
+ * relax.
36
34
  *
37
35
  * A discriminated union on `kind` rather than one object with optional
38
36
  * fields: an `equals` alert carrying a stray `threshold` would otherwise
@@ -98,8 +96,8 @@ export const alertState = z.enum(['ok', 'firing']);
98
96
  * cloud joins the two per request (`routes/alerts.ts`'s `toWire`).
99
97
  *
100
98
  * **There are no mail settings here.** The configuration format has no mail
101
- * fields, so no alert can be configured to send one, and a shape describing
102
- * recipients would describe a delivery path that does not exist.
99
+ * fields, so no alert can send one, and a shape describing recipients would
100
+ * describe a delivery path that does not exist.
103
101
  */
104
102
  export const datapointAlertRow = z.object({
105
103
  id: z.uuid(),
@@ -39,9 +39,9 @@ export declare const APP_USER_DISPLAY_NAME_MAX = 120;
39
39
  * buttons, where a hyphen is the conventional spelling — `azure-ad`, not
40
40
  * `azure_ad`.
41
41
  *
42
- * The two grammars are one character apart, which is exactly why this is its
43
- * own export with its own tests rather than a reuse: reusing the wrong one
44
- * would be invisible until a customer typed a hyphen.
42
+ * The two grammars are one character apart — why this is its own export with
43
+ * its own tests, not a reuse: reusing the wrong one would be invisible until
44
+ * a customer typed a hyphen.
45
45
  */
46
46
  export declare const providerSlug: z.ZodString;
47
47
  /**
@@ -338,9 +338,8 @@ export declare const APP_URL_PLACEHOLDERS: {
338
338
  * the mirror reason: it would mail every recipient the same link.
339
339
  *
340
340
  * **What it cannot check**: that the URL resolves, that the app serves that
341
- * path, or that the developer's page knows what to do with the token. Nothing
342
- * a schema can see says any of that, and a validator that looked sufficient
343
- * here would be read as an assurance.
341
+ * path, or that the developer's page knows what to do with the token — and a
342
+ * validator that looked sufficient here would be read as an assurance.
344
343
  */
345
344
  export declare function appUrlTemplate(placeholder: string): z.ZodString;
346
345
  /**
package/dist/app-users.js CHANGED
@@ -40,14 +40,14 @@ export const APP_USER_DISPLAY_NAME_MAX = 120;
40
40
  * buttons, where a hyphen is the conventional spelling — `azure-ad`, not
41
41
  * `azure_ad`.
42
42
  *
43
- * The two grammars are one character apart, which is exactly why this is its
44
- * own export with its own tests rather than a reuse: reusing the wrong one
45
- * would be invisible until a customer typed a hyphen.
43
+ * The two grammars are one character apart — why this is its own export with
44
+ * its own tests, not a reuse: reusing the wrong one would be invisible until
45
+ * a customer typed a hyphen.
46
46
  */
47
47
  export const providerSlug = z
48
48
  .string()
49
49
  .max(40)
50
- .regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, 'a provider slug is lowercase and hyphen-separated, starting with a letter');
50
+ .regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/, 'must be lowercase and hyphen-separated, starting with a letter');
51
51
  /**
52
52
  * **The three states an app user can be in, and the order is the lifecycle.**
53
53
  *
@@ -95,10 +95,10 @@ export const appUser = z.object({
95
95
  * accepted — it does not mean blocked and it does not mean without access.
96
96
  */
97
97
  has_password: z.boolean().meta({
98
- description: 'Whether this account has a Fleetless-held password at all. `false` is an identity-provider-only account, or an invitation not yet accepted — it does not mean blocked and it does not mean without access. No hash, no algorithm and no "last changed" travels here, and nothing on the wire can say whether a password is strong or already known to somebody else.',
98
+ description: 'Whether this account has a Fleetless-held password. `false` is an identity-provider-only account, or an invitation not yet accepted — it does not mean blocked and it does not mean without access. No hash, no algorithm and no "last changed" travels here, and nothing on the wire can say whether a password is strong or already known to somebody else.',
99
99
  }),
100
100
  providers: z.array(providerSlug).max(20).meta({
101
- description: 'The slugs of the identity providers this account is linked to, empty for a password-only user. It is what lets a developer\'s user list say where an account came from without a second request.',
101
+ description: 'The slugs of the identity providers this account is linked to, empty for a password-only user. Lets a developer\'s user list say where an account came from without a second request.',
102
102
  }),
103
103
  last_login_at: z.iso.datetime().nullable().meta({
104
104
  description: 'When this user last signed in, or `null` if they never have. Required and nullable rather than optional, so *never logged in* stays distinguishable from *this field was not loaded*.',
@@ -269,7 +269,7 @@ export const appOidcProvider = z.object({
269
269
  description: 'Whether a federated login may join an **existing** app user with the same address. It needs the provider to assert `email_verified` as well: either condition alone is account takeover, since a provider that lets anyone type any address into a profile would otherwise hand over every matching account, and a developer who connects a provider for a subset of their users would otherwise silently merge strangers.',
270
270
  }),
271
271
  enabled: z.boolean().meta({
272
- description: 'Whether this provider is offered at all. A disabled provider disappears from `GET /api/client/providers` and refuses a start with `provider_disabled`, without the row and its linked identities being deleted.',
272
+ description: 'Whether this provider is offered. A disabled provider disappears from `GET /api/client/providers` and refuses a start with `provider_disabled`, without the row and its linked identities being deleted.',
273
273
  }),
274
274
  created_at: z.iso.datetime().meta({ description: 'When the provider was configured, as an ISO 8601 timestamp.' }),
275
275
  }).strict();
@@ -371,9 +371,8 @@ export const APP_URL_PLACEHOLDERS = {
371
371
  * the mirror reason: it would mail every recipient the same link.
372
372
  *
373
373
  * **What it cannot check**: that the URL resolves, that the app serves that
374
- * path, or that the developer's page knows what to do with the token. Nothing
375
- * a schema can see says any of that, and a validator that looked sufficient
376
- * here would be read as an assurance.
374
+ * path, or that the developer's page knows what to do with the token — and a
375
+ * validator that looked sufficient here would be read as an assurance.
377
376
  */
378
377
  export function appUrlTemplate(placeholder) {
379
378
  return z
package/dist/apps.d.ts CHANGED
@@ -5,7 +5,7 @@ import { z } from 'zod';
5
5
  *
6
6
  * The rule that shapes all of this: **roles are the only filter**. A robot
7
7
  * assigned to an app exposes every one of its services to that app; what a
8
- * role does not grant simply does not exist for that user. There is no second
8
+ * role does not grant does not exist for that user. There is no second
9
9
  * visibility mechanism, and adding one later would create two places to look
10
10
  * when someone cannot see something.
11
11
  */
@@ -15,8 +15,8 @@ import { z } from 'zod';
15
15
  *
16
16
  * **Globally unique, not per org.** `clientLoginRequest` carries only the
17
17
  * identifier, the email and the password — there is no org context to
18
- * disambiguate with, so a per-org identifier could not be resolved at login
19
- * at all. A collision is refused with `identifier_taken`.
18
+ * disambiguate with, so a per-org identifier could not be resolved at login.
19
+ * A collision is refused with `identifier_taken`.
20
20
  */
21
21
  export declare const appIdentifier: z.ZodString;
22
22
  export declare const app: z.ZodObject<{
@@ -129,9 +129,8 @@ export declare const createServerKeyResponse: z.ZodObject<{
129
129
  export type CreateServerKeyResponse = z.infer<typeof createServerKeyResponse>;
130
130
  /**
131
131
  * Every app starts with `observe` and `operate`; custom roles are allowed
132
- * too. `builtin` marks the two starting roles — they may be
133
- * edited like any other, the flag exists so the console can explain where
134
- * they came from.
132
+ * too. `builtin` marks the two starting roles — editable like any other,
133
+ * the flag only tells the console where they came from.
135
134
  */
136
135
  export declare const role: z.ZodObject<{
137
136
  id: z.ZodUUID;
package/dist/apps.js CHANGED
@@ -6,7 +6,7 @@ import { slug } from './common.js';
6
6
  *
7
7
  * The rule that shapes all of this: **roles are the only filter**. A robot
8
8
  * assigned to an app exposes every one of its services to that app; what a
9
- * role does not grant simply does not exist for that user. There is no second
9
+ * role does not grant does not exist for that user. There is no second
10
10
  * visibility mechanism, and adding one later would create two places to look
11
11
  * when someone cannot see something.
12
12
  */
@@ -16,8 +16,8 @@ import { slug } from './common.js';
16
16
  *
17
17
  * **Globally unique, not per org.** `clientLoginRequest` carries only the
18
18
  * identifier, the email and the password — there is no org context to
19
- * disambiguate with, so a per-org identifier could not be resolved at login
20
- * at all. A collision is refused with `identifier_taken`.
19
+ * disambiguate with, so a per-org identifier could not be resolved at login.
20
+ * A collision is refused with `identifier_taken`.
21
21
  */
22
22
  export const appIdentifier = slug;
23
23
  export const app = z.object({
@@ -31,7 +31,7 @@ export const app = z.object({
31
31
  description: 'The display name, shown in the console and available to the developer\'s own pages through the `app.name` mail-template variable. Free text, changed through `PATCH /api/apps/:id`.',
32
32
  }),
33
33
  identifier: appIdentifier.meta({
34
- description: 'The stable handle a client sends at login, lowercase and underscore-separated. **Globally unique, not per organisation** — `clientLoginRequest` carries no org context to disambiguate with, so a collision is refused with `identifier_taken`.',
34
+ description: 'The stable handle a client sends at login, lowercase and underscore-separated. **Globally unique, not per organisation** — `clientLoginRequest` carries no org context, so a collision is refused with `identifier_taken`.',
35
35
  }),
36
36
  /** Robots are referenced individually; tags never grant rights. */
37
37
  robot_ids: z.array(z.uuid()).meta({
@@ -49,7 +49,7 @@ export const app = z.object({
49
49
  * (`createAppUserRequest`, `createAppInvitationRequest`) rather than
50
50
  * pre-filling a form.
51
51
  *
52
- * That is worth saying out loud because it changes what a wrong value costs.
52
+ * Worth saying: it changes what a wrong value costs.
53
53
  * A prefill somebody can see and correct became a default applied on the
54
54
  * server, and the obvious next question — should it be required instead? —
55
55
  * has a deliberate answer: no, because an invitation resolves the role at
@@ -66,7 +66,7 @@ export const app = z.object({
66
66
  * check that, so `PATCH /api/apps/:id` does.
67
67
  */
68
68
  default_role_id: z.uuid().nullable().meta({
69
- description: 'The role an app user gets when they are created or invited without an explicit one. `null` means this app has not chosen a default, the normal state of an app created before its roles were configured — and then a create or invite that omits `role_id` is a `validation_error` rather than a user with no role. An invitation resolves the role when it is issued, so changing this never re-aims an outstanding one. The role must belong to this app, which `PATCH /api/apps/:id` checks and the schema cannot.',
69
+ description: 'The role an app user gets when created or invited without an explicit one. `null` means this app has not chosen a default, the normal state of an app created before its roles were configured — and then a create or invite that omits `role_id` gets `validation_error`, not a user with no role. An invitation resolves the role when issued, so changing this never re-aims an outstanding one. The role must belong to this app, which `PATCH /api/apps/:id` checks and the schema cannot.',
70
70
  }),
71
71
  created_at: z.iso.datetime().meta({
72
72
  description: 'When the app was created, as an ISO 8601 timestamp. `GET /api/apps` orders by this field.',
@@ -145,7 +145,7 @@ export const updateAppRequest = z.object({
145
145
  export const serverKeyToken = z.string().regex(/^flk_[0-9a-f]{32}$/);
146
146
  export const serverKey = z.object({
147
147
  id: z.uuid().meta({
148
- description: 'The key row, and what the rotate and delete routes address. It is not the key: the secret itself is never carried by this shape.',
148
+ description: 'The key row, and what the rotate and delete routes address. It is not the key: this shape never carries the secret.',
149
149
  }),
150
150
  app_id: z.uuid().meta({
151
151
  description: 'The app whose full rights this key carries. A key is never shared between apps.',
@@ -164,7 +164,7 @@ export const serverKey = z.object({
164
164
  /** What `GET /api/apps/:id/server-keys` answers — metadata only; the raw secret exists once, in `createServerKeyResponse`, and never here. */
165
165
  export const serverKeyListResponse = z.object({
166
166
  server_keys: z.array(serverKey).meta({
167
- description: 'The app\'s server keys as metadata, oldest first by `created_at`. The raw secret is not here and never will be: it exists once, in the answer to the request that created or rotated the key.',
167
+ description: 'The app\'s server keys as metadata, oldest first by `created_at`. The raw secret is not here and never will be: it exists once, in the response that created or rotated the key.',
168
168
  }),
169
169
  });
170
170
  export const createServerKeyResponse = z.object({
@@ -173,9 +173,8 @@ export const createServerKeyResponse = z.object({
173
173
  });
174
174
  /**
175
175
  * Every app starts with `observe` and `operate`; custom roles are allowed
176
- * too. `builtin` marks the two starting roles — they may be
177
- * edited like any other, the flag exists so the console can explain where
178
- * they came from.
176
+ * too. `builtin` marks the two starting roles — editable like any other,
177
+ * the flag only tells the console where they came from.
179
178
  */
180
179
  export const role = z.object({
181
180
  id: z.uuid().meta({
@@ -188,7 +187,7 @@ export const role = z.object({
188
187
  description: 'The role\'s name, shown wherever a user\'s access is chosen. The two roles every app starts with are named `observe` and `operate`.',
189
188
  }),
190
189
  builtin: z.boolean().meta({
191
- description: '`true` for the two roles every app starts with. Their **rights may be re-scoped** exactly like a custom role\'s, through `PUT /api/apps/:id/roles/:roleId/permissions` — the flag exists so the console can explain where they came from, not to protect them. It does not make them renamable or deletable, because no route renames or deletes any role.',
190
+ description: '`true` for the two roles every app starts with. Their **rights may be re-scoped** exactly like a custom role\'s, through `PUT /api/apps/:id/roles/:roleId/permissions` — the flag exists so the console can explain where they came from, not to protect them. It does not make them renamable or deletable — no route does that for any role.',
192
191
  }),
193
192
  });
194
193
  /** What `GET /api/apps/:id/roles` answers: the app's roles, builtin and custom alike. */
package/dist/assets.js CHANGED
@@ -70,7 +70,7 @@ export const asset = z.object({
70
70
  id: z.uuid().meta({ description: 'The asset\'s id in the store.' }),
71
71
  robot_id: z.uuid().meta({ description: 'The robot this asset belongs to.' }),
72
72
  kind: assetKind.meta({
73
- description: 'What the file is: the `urdf` itself, a `mesh` it references, a `texture` a mesh or the URDF paints with, or `other`. A renderer decides from this alone, before fetching anything, what it has to pre-fetch.',
73
+ description: 'What the file is: the `urdf` itself, a `mesh` it references, a `texture` a mesh or the URDF paints with, or `other`. A renderer decides from this alone, before fetching anything, what to pre-fetch.',
74
74
  }),
75
75
  /**
76
76
  * What the robot called it — for a mesh, the `package://` URI the URDF
@@ -105,7 +105,7 @@ export const asset = z.object({
105
105
  * rules, one enforced and one only documented, is how a traversal gets in.
106
106
  */
107
107
  name: z.string().min(1).max(500).meta({
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.',
108
+ description: 'What the robot called it — for a mesh, the `package://` URI the URDF references, verbatim, 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.',
109
109
  }),
110
110
  media_type: z.string().min(1).max(120).meta({
111
111
  description: 'The media type of the stored bytes, as the producer reported it.',
@@ -122,7 +122,7 @@ export const asset = z.object({
122
122
  * predictable failure of a store that hides it.
123
123
  */
124
124
  sha256: z.string().regex(/^[a-f0-9]{64}$/).meta({
125
- description: 'The content hash, lowercase hex, and the reason two robots sharing a mesh cost one copy. It is exposed because it is the only way a client can tell "this is the same mesh I already have" across robots.',
125
+ description: 'The content hash, lowercase hex. Exposed because it is the only way a client can tell "this is the same mesh I already have" across robots — the reason two robots sharing a mesh cost one copy.',
126
126
  }),
127
127
  created_at: z.iso.datetime().meta({
128
128
  description: 'When the asset was first stored, as an ISO 8601 timestamp.',
@@ -169,13 +169,13 @@ export const urdfCompleteness = z.object({
169
169
  */
170
170
  missing: z.array(z.object({
171
171
  uri: z.string().min(1).max(500).meta({
172
- description: 'The reference, verbatim, that no stored asset answers — a `package://` URI the workspace does not hold, or an absolute or bare relative path nothing will ever fetch. A developer whose URDF names one of the latter is entitled to be told so.',
172
+ description: 'The reference, verbatim, that no stored asset answers — a `package://` URI the workspace does not hold, or an absolute or bare relative path nothing will ever fetch.',
173
173
  }),
174
174
  element: z.enum(['mesh', 'texture']).meta({
175
175
  description: 'Which kind of reference it was: geometry the URDF names as a `mesh`, or a `texture` a surface paints with. Without it a client reports a missing texture as a missing mesh, contradicting `mesh_count` beside it.',
176
176
  }),
177
177
  })).meta({
178
- description: 'The references nothing in the store answers, each with the element that asked for it. A bare count is a dead end that sends a developer hunting through a workspace by hand; the references are what they can act on, so the references travel.',
178
+ description: 'The references nothing in the store answers, each with the element that asked for it. A bare count would send a developer hunting through the workspace by hand; the references are what they can act on.',
179
179
  }),
180
180
  });
181
181
  /**
@@ -193,7 +193,7 @@ export const urdfCompleteness = z.object({
193
193
  */
194
194
  export const assetSyncRequest = z.object({
195
195
  source: z.enum(['bridge']).meta({
196
- description: 'Where the bytes come from. `bridge` is the only value today: the connected bridge reads them from the robot\'s own workspace. It is validated rather than ignored, so a caller naming a source that does not exist yet learns that instead of silently getting a bridge sync.',
196
+ description: 'Where the bytes come from. `bridge` is the only value today: the connected bridge reads them from the robot\'s own workspace. Validated rather than ignored, so a caller naming an unknown source is told so instead of silently getting a bridge sync.',
197
197
  }),
198
198
  }).strict();
199
199
  export const assetSyncResponse = z.object({
@@ -253,7 +253,7 @@ export const assetTooLargeDetails = z.object({
253
253
  description: 'The upload ceiling, in bytes.',
254
254
  }),
255
255
  size_bytes: z.number().int().positive().meta({
256
- description: 'How large the refused file actually is, in bytes. With `limit_bytes` beside it a developer can tell whether to shrink the mesh or raise the limit; "too large" alone answers neither.',
256
+ description: 'How large the refused file is, in bytes. With `limit_bytes` beside it a developer can tell whether to shrink the mesh or raise the limit; "too large" alone answers neither.',
257
257
  }),
258
258
  });
259
259
  /**
@@ -312,9 +312,8 @@ export const assetFailure = z.object({
312
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.',
313
313
  }),
314
314
  }).superRefine((f, ctx) => {
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.
315
+ // Enforced here, not merely described above — a rule that lives only in a
316
+ // comment gets filled with something else.
318
317
  if (f.kind === 'too_large' && f.details == null) {
319
318
  ctx.addIssue({ code: 'custom', path: ['details'], message: '`too_large` without limit_bytes/size_bytes says nothing a developer can act on' });
320
319
  }
@@ -377,8 +376,6 @@ export const assetListResponse = z.object({
377
376
  description: 'Every asset stored for this robot: the URDF, the meshes it references, and the textures those paint with.',
378
377
  }),
379
378
  /**
380
- * The sync running right now, or `null`.
381
- *
382
379
  * **This field exists for the reload case.** A client that holds the
383
380
  * `sync_id` only in memory loses its progress display on a refresh, and the
384
381
  * state is still there server-side under `GET .../assets/sync/<id>` —
@@ -409,7 +406,7 @@ export const assetListResponse = z.object({
409
406
  * crash, and no amount of polling shortens it.
410
407
  */
411
408
  urdf_available: z.boolean().nullable().meta({
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.',
409
+ description: 'What the connected bridge says it *could* transfer — deliberately separate from what has been transferred. `null` when no bridge is connected, distinct from `false`: "no robot is online to ask" and "the robot has no URDF" send a developer to 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.',
413
410
  }),
414
411
  });
415
412
  /**
package/dist/audit.d.ts CHANGED
@@ -14,21 +14,19 @@ import { z } from 'zod';
14
14
  * log — an email, a key name, a robot name — so the console never has to
15
15
  * resolve four different id kinds to render a row.
16
16
  *
17
- * **`end_user` stays, and it stays for the rows already written.** The
18
- * split into two identity spaces replaced the org's one user pool with
19
- * Fleetless users and per-app app users; every new row an app user writes
20
- * carries `app_user`. But an audit log is the one thing this platform must
21
- * never rewrite, and there are stored rows whose `kind` is `end_user`. Dropping
22
- * the member would leave those rows failing their own schema — a log that
23
- * cannot be read back is worse than one carrying a retired word.
17
+ * **`end_user` stays, for the rows already written.** The identity split
18
+ * replaced the org's one user pool with Fleetless users and per-app app
19
+ * users; new rows from an app user carry `app_user`. But an audit log must
20
+ * never be rewritten, and stored rows still have `kind: end_user` — dropping
21
+ * the member would fail their own schema. A log that can't be read back is
22
+ * worse than one carrying a retired word.
24
23
  *
25
- * So this enum is deliberately **wider than what any producer emits**: nothing
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.
24
+ * So the enum is deliberately **wider than what any producer emits**:
25
+ * nothing writes `end_user` any more, and nothing should start again — said
26
+ * here rather than left for a future search to infer.
28
27
  *
29
28
  * `developer` is a Fleetless user. It kept its name through both redesigns
30
- * because it was always right about what it named: the person who configures
31
- * robots.
29
+ * because it was always right: the person who configures robots.
32
30
  */
33
31
  export declare const auditActor: z.ZodObject<{
34
32
  kind: z.ZodEnum<{
package/dist/audit.js CHANGED
@@ -15,21 +15,19 @@ import { wireSeqCursor, wireTimestampMs } from './common.js';
15
15
  * log — an email, a key name, a robot name — so the console never has to
16
16
  * resolve four different id kinds to render a row.
17
17
  *
18
- * **`end_user` stays, and it stays for the rows already written.** The
19
- * split into two identity spaces replaced the org's one user pool with
20
- * Fleetless users and per-app app users; every new row an app user writes
21
- * carries `app_user`. But an audit log is the one thing this platform must
22
- * never rewrite, and there are stored rows whose `kind` is `end_user`. Dropping
23
- * the member would leave those rows failing their own schema — a log that
24
- * cannot be read back is worse than one carrying a retired word.
18
+ * **`end_user` stays, for the rows already written.** The identity split
19
+ * replaced the org's one user pool with Fleetless users and per-app app
20
+ * users; new rows from an app user carry `app_user`. But an audit log must
21
+ * never be rewritten, and stored rows still have `kind: end_user` — dropping
22
+ * the member would fail their own schema. A log that can't be read back is
23
+ * worse than one carrying a retired word.
25
24
  *
26
- * So this enum is deliberately **wider than what any producer emits**: nothing
27
- * writes `end_user` any more, and nothing may start again. That is said here
28
- * rather than left to be inferred from a search somebody runs in a year.
25
+ * So the enum is deliberately **wider than what any producer emits**:
26
+ * nothing writes `end_user` any more, and nothing should start again — said
27
+ * here rather than left for a future search to infer.
29
28
  *
30
29
  * `developer` is a Fleetless user. It kept its name through both redesigns
31
- * because it was always right about what it named: the person who configures
32
- * robots.
30
+ * because it was always right: the person who configures robots.
33
31
  */
34
32
  export const auditActor = z.object({
35
33
  kind: z.enum(['developer', 'end_user', 'app_user', 'server_key', 'bridge']),
@@ -130,12 +128,11 @@ export const auditQuery = z.object({
130
128
  * Everything under a dotted prefix, e.g. `server_key.` for all three
131
129
  * server-key actions.
132
130
  *
133
- * **A separate parameter, not a widening of `action`.** The sentence on
134
- * `action` above — a filter that matches more than it says is not one —
135
- * still stands; this is a different question with a name that says which
136
- * one it is. Setting both is refused rather than resolved, because a query
131
+ * **A separate parameter, not a widening of `action`.** The rule on
132
+ * `action` above still stands; this is a different question with a name
133
+ * that says which one it is. Setting both is refused, not resolved —
137
134
  * naming an exact action *and* a prefix is a caller mistake, not a
138
- * combination anyone should have to guess the meaning of.
135
+ * combination to guess the meaning of.
139
136
  *
140
137
  * **The published artifact cannot express that refusal**: a cross-field
141
138
  * `.refine()` has no JSON Schema rendering, so `audit-query.schema.json`
@@ -48,13 +48,12 @@ export type ClientRefreshRequest = z.infer<typeof clientRefreshRequest>;
48
48
  *
49
49
  * **The route answers `204` and has no response shape.** It used to answer a
50
50
  * `clientLogoutResponse` reporting what was left of the session at the identity
51
- * provider — RP-initiated logout, an `end_session_endpoint` to redirect to,
52
- * four ways of saying "we cannot end that session". That whole apparatus
53
- * belonged to the hosted login flow, where Fleetless owned the browser. It does
54
- * not own it any more: the developer's app does, and an app that wants to end
55
- * a provider session redirects there itself, knowing its own provider, which
56
- * Fleetless never did better than it. Listed as a breaking change rather than
57
- * quietly kept as a field nobody fills.
51
+ * provider — RP-initiated logout, an `end_session_endpoint`, four ways of
52
+ * saying "we cannot end that session". That belonged to the hosted login flow,
53
+ * where Fleetless owned the browser; it doesn't any more. The developer's app
54
+ * does, and knows its own provider better than Fleetless ever did — ending a
55
+ * provider session is its own redirect to make. Listed as a breaking change,
56
+ * not quietly kept as a field nobody fills.
58
57
  */
59
58
  export declare const clientLogoutRequest: z.ZodObject<{
60
59
  refresh_token: z.ZodString;
@@ -110,8 +109,8 @@ export type ClientResendVerificationRequest = z.infer<typeof clientResendVerific
110
109
  * to be. An **app identifier** no app carries is the one refusal, `404
111
110
  * not_found`, because an identifier is public and an address is not.
112
111
  *
113
- * Moved here from `identity.ts`, where it sat because the client surface had no
114
- * file of its own for it. It is an app-user shape and belongs with them.
112
+ * Moved here from `identity.ts`, which predates a client-surface file — it's
113
+ * an app-user shape and belongs with them.
115
114
  */
116
115
  export declare const clientPasswordResetRequest: z.ZodObject<{
117
116
  app_identifier: z.ZodString;
@@ -37,7 +37,7 @@ import { password } from './identity.js';
37
37
  /* ------------------------------------------------------ password login -- */
38
38
  export const clientLoginRequest = z.object({
39
39
  app_identifier: appIdentifier.meta({
40
- description: 'The app being logged in to, as its globally unique identifier — the lowercase, underscore-separated string the developer chose when the app was created. There is no organisation context at login, so this is what decides which app the credentials are checked for.',
40
+ description: 'The app being logged in to: its globally unique, lowercase, underscore-separated identifier, chosen by the developer at creation. There is no organisation context at login, so this is what decides which app the credentials are checked for.',
41
41
  }),
42
42
  email: z.email().meta({
43
43
  description: 'The app user\'s address. Addresses are unique **per app**, not across Fleetless: the same address may be an unrelated account in another app of the same organisation, so this pair is what identifies a person here.',
@@ -58,17 +58,16 @@ export const clientRefreshRequest = z.object({
58
58
  *
59
59
  * **The route answers `204` and has no response shape.** It used to answer a
60
60
  * `clientLogoutResponse` reporting what was left of the session at the identity
61
- * provider — RP-initiated logout, an `end_session_endpoint` to redirect to,
62
- * four ways of saying "we cannot end that session". That whole apparatus
63
- * belonged to the hosted login flow, where Fleetless owned the browser. It does
64
- * not own it any more: the developer's app does, and an app that wants to end
65
- * a provider session redirects there itself, knowing its own provider, which
66
- * Fleetless never did better than it. Listed as a breaking change rather than
67
- * quietly kept as a field nobody fills.
61
+ * provider — RP-initiated logout, an `end_session_endpoint`, four ways of
62
+ * saying "we cannot end that session". That belonged to the hosted login flow,
63
+ * where Fleetless owned the browser; it doesn't any more. The developer's app
64
+ * does, and knows its own provider better than Fleetless ever did — ending a
65
+ * provider session is its own redirect to make. Listed as a breaking change,
66
+ * not quietly kept as a field nobody fills.
68
67
  */
69
68
  export const clientLogoutRequest = z.object({
70
69
  refresh_token: z.string().min(1).meta({
71
- description: 'Any refresh token of the session to end. The whole token family is revoked server-side, so a token stolen before this call stops working too — clearing a client-side store is a gesture, not a revocation. The answer is `204`: a token the server does not recognise gets it too, since the end state a caller asked for is the end state they get.',
70
+ description: 'Any refresh token of the session to end. The whole token family is revoked server-side, so a token stolen before this call stops working too — clearing a client-side store is a gesture, not a revocation. The answer is `204`: a token the server does not recognise gets it too, since that is the end state being asked for.',
72
71
  }),
73
72
  });
74
73
  /* ---------------------------------------------- registration and mails -- */
@@ -137,8 +136,8 @@ export const clientResendVerificationRequest = z
137
136
  * to be. An **app identifier** no app carries is the one refusal, `404
138
137
  * not_found`, because an identifier is public and an address is not.
139
138
  *
140
- * Moved here from `identity.ts`, where it sat because the client surface had no
141
- * file of its own for it. It is an app-user shape and belongs with them.
139
+ * Moved here from `identity.ts`, which predates a client-surface file — it's
140
+ * an app-user shape and belongs with them.
142
141
  */
143
142
  export const clientPasswordResetRequest = z
144
143
  .object({
@@ -373,7 +372,7 @@ export const clientMcpInteraction = z.object({
373
372
  app_id: z.uuid().meta({ description: 'The app this authorization is for. The approving token\'s `app_id` must match it — an interaction of one app cannot be approved with a session from another.' }),
374
373
  client_name: z.string().nullable().meta({ description: 'What the MCP client calls itself, or `null` if it named nothing. **Unverified** — see `client_name_verified`.' }),
375
374
  client_name_verified: z.literal(false).meta({
376
- description: 'Always `false`. The client registered itself without authentication and chose this name about itself, so it must be rendered as a claim and never as an identity. There is no verified case, which is why this is a literal and not a boolean: a `true` branch would be dead code that looked like a safeguard.',
375
+ description: 'Always `false`. The client registered itself without authentication and named itself, so it must be rendered as a claim and never as an identity. There is no verified case, which is why this is a literal and not a boolean: a `true` branch would be dead code that looked like a safeguard.',
377
376
  }),
378
377
  scopes: z.array(z.string()).meta({ description: 'The scopes the client asked for, to show the person before they approve.' }),
379
378
  already_granted: z.boolean().meta({ description: 'Whether this user has already approved this client. It is a record of what they answered last time, and **this route makes no second use of it**: an app that skips its own consent screen when this is `true` is the only thing deciding that, and approve succeeds identically for a user who holds no grant at all. The standing grant is read elsewhere, on every request to the app\'s MCP endpoint. Withdrawing it is `DELETE /api/client/mcp/grants/:clientId` for the person themselves and `DELETE /api/apps/:id/users/:userId/mcp-grants/:clientId` for the developer. A withdrawal makes this `false` again at the next authorization **and stops the client at its very next MCP call**, unexpired access token and all — up to fifteen minutes of it — because the endpoint keys that check on the `client_id` the token carries.' }),
@@ -417,10 +416,10 @@ export const mcpConsentGrant = z.object({
417
416
  description: 'The MCP client this consent is for, as its dynamic registration was issued. It is the value the withdrawal routes take in their path, and it is the only stable handle on a client — the name beside it is not one.',
418
417
  }),
419
418
  client_name: z.string().nullable().meta({
420
- description: 'What the client calls itself, or `null` when its registration is gone and there is no longer anything to have named. **Unverified** — see `client_name_verified`.',
419
+ description: 'What the client calls itself, or `null` once its registration is gone. **Unverified** — see `client_name_verified`.',
421
420
  }),
422
421
  client_name_verified: z.literal(false).meta({
423
- description: 'Always `false`. The client registered itself without authentication and chose this name about itself, so it must be rendered as a claim and never as an identity. There is no verified case, which is why this is a literal and not a boolean: a `true` branch would be dead code that looked like a safeguard.',
422
+ description: 'Always `false`. The client registered itself without authentication and named itself, so it must be rendered as a claim and never as an identity. There is no verified case, which is why this is a literal and not a boolean: a `true` branch would be dead code that looked like a safeguard.',
424
423
  }),
425
424
  granted_at: z.iso.datetime().meta({
426
425
  description: 'When the consent was last given. A withdrawal followed by a fresh approval moves it, because the second approval is the agreement that stands — it is not a record of the first time anybody ever said yes.',
@@ -463,7 +462,7 @@ export const mcpConsentGrantListResponse = z.object({
463
462
  */
464
463
  export const clientIdentity = z.object({
465
464
  kind: z.enum(['developer', 'app_user', 'server_key']).meta({
466
- description: 'Which of the three kinds of caller this is: a `developer` working through the console, an `app_user` holding a token from a client login, or a `server_key` used by server-side code. Stated outright rather than left to be inferred from which id happens to be set.',
465
+ description: 'Which of the three kinds of caller this is: a `developer` working through the console, an `app_user` holding a token from a client login, or a `server_key` used by server-side code. Stated outright, not inferred from which id is set.',
467
466
  }),
468
467
  developer_id: z.uuid().nullable().meta({
469
468
  description: 'The Fleetless user behind this session, or `null` when `kind` is not `developer`.',