@fleetless/contracts 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +97 -2
- package/CODE_OF_CONDUCT.md +83 -0
- package/CONTRIBUTING.md +136 -0
- package/README.md +49 -13
- package/SECURITY.md +55 -0
- package/artifacts/openapi.json +11 -11
- package/artifacts/routes.json +12 -12
- package/artifacts/schema/create-app-oidc-provider-request.schema.json +1 -1
- package/dist/alerts.d.ts +23 -28
- package/dist/alerts.js +23 -29
- package/dist/app-users.d.ts +18 -19
- package/dist/app-users.js +18 -20
- package/dist/apps.d.ts +21 -25
- package/dist/apps.js +42 -52
- package/dist/assets.d.ts +70 -132
- package/dist/assets.js +130 -223
- package/dist/audit.d.ts +14 -15
- package/dist/audit.js +28 -55
- package/dist/client-auth.d.ts +9 -9
- package/dist/client-auth.js +8 -9
- package/dist/common.d.ts +29 -37
- package/dist/common.js +28 -37
- package/dist/config-issues.d.ts +23 -25
- package/dist/config-issues.js +17 -17
- package/dist/config.d.ts +37 -44
- package/dist/config.js +145 -187
- package/dist/errors.d.ts +4 -3
- package/dist/errors.js +83 -116
- package/dist/identity.d.ts +24 -27
- package/dist/identity.js +23 -27
- package/dist/index.d.ts +4 -4
- package/dist/index.js +14 -15
- package/dist/introspection.d.ts +7 -6
- package/dist/introspection.js +6 -6
- package/dist/jobs.d.ts +16 -16
- package/dist/jobs.js +24 -29
- package/dist/mcp.d.ts +14 -15
- package/dist/mcp.js +12 -14
- package/dist/oauth.d.ts +21 -27
- package/dist/oauth.js +33 -43
- package/dist/protocol.d.ts +51 -62
- package/dist/protocol.js +107 -139
- package/dist/realtime.d.ts +53 -68
- package/dist/realtime.js +78 -104
- package/dist/rest.d.ts +183 -244
- package/dist/rest.js +305 -399
- package/dist/routes.d.ts +4 -3
- package/dist/routes.js +33 -32
- package/package.json +12 -7
package/dist/app-users.js
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { idpIssuer, mailStatus, password } from './identity.js';
|
|
4
4
|
/**
|
|
5
|
-
* **App users: the per-app identity space
|
|
6
|
-
* D1–D7).
|
|
5
|
+
* **App users: the per-app identity space.**
|
|
7
6
|
*
|
|
8
7
|
* The 2026-08-29 model put developers and end users into one pool per org,
|
|
9
8
|
* tied apps to groups, and let an org admin enter an app only by
|
|
@@ -22,7 +21,7 @@ import { idpIssuer, mailStatus, password } from './identity.js';
|
|
|
22
21
|
* as unrelated accounts, and a Fleetless user who wants to use an app
|
|
23
22
|
* registers or is invited like anybody else.
|
|
24
23
|
*
|
|
25
|
-
* **Fleetless shows an app user no page
|
|
24
|
+
* **Fleetless shows an app user no page**. The developer's own UI owns
|
|
26
25
|
* every screen and calls the JSON client-auth API (`client-auth.ts`). The one
|
|
27
26
|
* Fleetless-rendered surface an app user can reach is the problem page for an
|
|
28
27
|
* OIDC callback whose state no longer resolves to a redirect URI — every other
|
|
@@ -52,12 +51,12 @@ export const providerSlug = z
|
|
|
52
51
|
/**
|
|
53
52
|
* **The three states an app user can be in, and the order is the lifecycle.**
|
|
54
53
|
*
|
|
55
|
-
* - `pending_verification` — self-registered, mail sent, cannot log in yet
|
|
56
|
-
*
|
|
57
|
-
*
|
|
54
|
+
* - `pending_verification` — self-registered, mail sent, cannot log in yet.
|
|
55
|
+
* Without this state a domain allow-list would prove nothing: anybody could
|
|
56
|
+
* claim any address at an allowed domain.
|
|
58
57
|
* - `active` — may log in.
|
|
59
58
|
* - `blocked` — may not, and every refusal is the same `invalid_credentials`
|
|
60
|
-
* a wrong password gets
|
|
59
|
+
* a wrong password gets. A block that announced itself would be an
|
|
61
60
|
* account-enumeration oracle with an extra step.
|
|
62
61
|
*
|
|
63
62
|
* `pending_verification` is reached exactly once and left only by spending the
|
|
@@ -68,7 +67,7 @@ export const appUserStatus = z.enum(['pending_verification', 'active', 'blocked'
|
|
|
68
67
|
* **A user of one app.** Not a user of the org: `app_id` is the whole scope,
|
|
69
68
|
* and the uniqueness constraint the cloud enforces is `(app_id, lower(email))`
|
|
70
69
|
* rather than a global one. The same person at two apps of one org is two
|
|
71
|
-
* unrelated rows, by design
|
|
70
|
+
* unrelated rows, by design.
|
|
72
71
|
*/
|
|
73
72
|
export const appUser = z.object({
|
|
74
73
|
id: z.uuid().meta({
|
|
@@ -148,8 +147,8 @@ export const createAppUserRequest = z
|
|
|
148
147
|
* offering it is a refusal rather than a silently dropped field.
|
|
149
148
|
*
|
|
150
149
|
* **`status` admits only `active` and `blocked`.** `pending_verification` is
|
|
151
|
-
* reached once, by self-registration, and left by spending the mailed token
|
|
152
|
-
*
|
|
150
|
+
* reached once, by self-registration, and left by spending the mailed token.
|
|
151
|
+
* A developer able to set it back could void a verified address without
|
|
153
152
|
* the user ever seeing a mail, and there is no route out of that state that
|
|
154
153
|
* does not require a token nobody re-sent. So the narrower enum is the rule,
|
|
155
154
|
* stated in the schema rather than left to a handler to remember.
|
|
@@ -196,7 +195,7 @@ export const createAppInvitationRequest = z
|
|
|
196
195
|
* An app that has configured none has nowhere for it to point, so there is no
|
|
197
196
|
* link to hand back — `null` says that outright, where an absent key would be
|
|
198
197
|
* indistinguishable from a mapper that dropped the field and a fabricated
|
|
199
|
-
* Fleetless-hosted URL would name a page this product does not serve
|
|
198
|
+
* Fleetless-hosted URL would name a page this product does not serve.
|
|
200
199
|
*/
|
|
201
200
|
export const appInvitation = z.object({
|
|
202
201
|
id: z.uuid().meta({ description: 'The invitation, as listed and revoked by the developer.' }),
|
|
@@ -231,7 +230,7 @@ export const appInvitationListResponse = z.object({
|
|
|
231
230
|
}),
|
|
232
231
|
});
|
|
233
232
|
/**
|
|
234
|
-
* **An app's OIDC provider, as read back
|
|
233
|
+
* **An app's OIDC provider, as read back**. Any number per app, unlike
|
|
235
234
|
* the group provider this replaces — a developer serving two customers needs
|
|
236
235
|
* two, and the old at-most-one rule was a property of groups rather than of
|
|
237
236
|
* identity.
|
|
@@ -305,7 +304,7 @@ export const createAppOidcProviderRequest = z
|
|
|
305
304
|
description: 'The client secret, **write-only**: it is stored encrypted and comes back through nothing — not the read, not this route\'s own answer, not an audit detail. Required on create, since a provider with no secret cannot exchange a code; the minimum length refuses a value that is a misconfiguration rather than a secret.',
|
|
306
305
|
}),
|
|
307
306
|
scopes: z.array(z.string().min(1).max(60)).min(1).max(20).default(['openid', 'email', 'profile']).meta({
|
|
308
|
-
description: 'The scopes to request. Defaults to `openid email profile`, which is what
|
|
307
|
+
description: 'The scopes to request. Defaults to `openid email profile`, which is what account linking actually reads: the subject, the address and its verified flag, and a name.',
|
|
309
308
|
}),
|
|
310
309
|
link_verified_emails: z.boolean().default(false).meta({
|
|
311
310
|
description: 'Whether a federated login may join an existing app user by verified address. **Defaults to off**, because relaxing later is additive and admitting duplicates now and tightening afterwards is not.',
|
|
@@ -438,16 +437,15 @@ export const emailDomain = z
|
|
|
438
437
|
.max(253)
|
|
439
438
|
.regex(/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/, 'must be a lowercase domain name with at least two labels');
|
|
440
439
|
/**
|
|
441
|
-
* **The app's auth settings: one row per app, configured by a Fleetless user
|
|
442
|
-
* (D3).
|
|
440
|
+
* **The app's auth settings: one row per app, configured by a Fleetless user.**
|
|
443
441
|
*
|
|
444
442
|
* `self_registration` and `allowed_domains` are **one policy for one
|
|
445
443
|
* decision** — they govern registration by password and registration through
|
|
446
|
-
* an identity provider alike
|
|
444
|
+
* an identity provider alike. An invitation always bypasses both, because
|
|
447
445
|
* a developer inviting somebody by hand has already made the decision the
|
|
448
446
|
* whitelist automates.
|
|
449
447
|
*
|
|
450
|
-
* The four URLs are what makes
|
|
448
|
+
* The four URLs are what makes that work: Fleetless mails a link, and the link
|
|
451
449
|
* points into the developer's app. An app that has configured none of them
|
|
452
450
|
* still works for password login — it simply cannot send a mail that leads
|
|
453
451
|
* anywhere, and `send_mail` is refused rather than silently sending a dead
|
|
@@ -494,7 +492,7 @@ export const putAppAuthConfigRequest = appAuthConfig
|
|
|
494
492
|
.omit({ oidc_callback_url: true, updated_at: true })
|
|
495
493
|
.strict();
|
|
496
494
|
/**
|
|
497
|
-
* The three mails a developer may replace with their own template
|
|
495
|
+
* The three mails a developer may replace with their own template.
|
|
498
496
|
* Mails to *Fleetless* users — a team invitation, a console password reset —
|
|
499
497
|
* stay Fleetless default and are deliberately not customisable: they are
|
|
500
498
|
* about this platform, not about the developer's product.
|
|
@@ -519,7 +517,7 @@ export const MAIL_TEMPLATE_VARIABLES = [
|
|
|
519
517
|
'expires_in_hours',
|
|
520
518
|
];
|
|
521
519
|
/**
|
|
522
|
-
* **The Fleetless default text for the three app mails
|
|
520
|
+
* **The Fleetless default text for the three app mails.**
|
|
523
521
|
*
|
|
524
522
|
* It lives here rather than in the cloud because two products send the same
|
|
525
523
|
* words: the cloud renders these when an app has no template of its own, and
|
|
@@ -553,7 +551,7 @@ export const MAIL_TEMPLATE_VARIABLES = [
|
|
|
553
551
|
* that one is written by an authenticated developer about somebody they
|
|
554
552
|
* invited.
|
|
555
553
|
*
|
|
556
|
-
* **`expires_in_hours` is the only lifetime variable
|
|
554
|
+
* **`expires_in_hours` is the only lifetime variable a template gets**, and
|
|
557
555
|
* the three values are 1, 24 and 168. "The next 168 hours" is not how a person
|
|
558
556
|
* says a week, so each default converts: 48 and up reads in days, exactly one
|
|
559
557
|
* reads "1 hour", everything else reads in hours. The conversion is in the
|
package/dist/apps.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
1
2
|
import { z } from 'zod';
|
|
2
3
|
/**
|
|
3
|
-
* Apps, roles and rights
|
|
4
|
+
* Apps, roles and rights.
|
|
4
5
|
*
|
|
5
6
|
* The rule that shapes all of this: **roles are the only filter**. A robot
|
|
6
7
|
* assigned to an app exposes every one of its services to that app; what a
|
|
@@ -42,12 +43,11 @@ export declare const appListResponse: z.ZodObject<{
|
|
|
42
43
|
}, z.core.$strip>;
|
|
43
44
|
export type AppListResponse = z.infer<typeof appListResponse>;
|
|
44
45
|
/**
|
|
45
|
-
* **`robot_ids` is accepted here, and `.strict()` catches everything else
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* definition of a shape that reads as though it does something it does not.
|
|
46
|
+
* **`robot_ids` is accepted here, and `.strict()` catches everything else.**
|
|
47
|
+
* A create shape carrying `name` and `identifier` only would let zod strip an
|
|
48
|
+
* offered `robot_ids`, so a caller creating an app *with* robots gets a `201`
|
|
49
|
+
* and an app with none — a shape that reads as though it does something it does
|
|
50
|
+
* not.
|
|
51
51
|
*
|
|
52
52
|
* Both halves matter and neither alone is enough. Accepting `robot_ids` is
|
|
53
53
|
* right because attaching robots at creation is the obvious operation and the
|
|
@@ -69,21 +69,17 @@ export type CreateAppRequest = z.infer<typeof createAppRequest>;
|
|
|
69
69
|
/**
|
|
70
70
|
* **`.strict()` is what makes an absent field mean something here.**
|
|
71
71
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* been carried one shape over. A caller who sends a field this route does not
|
|
78
|
-
* do is asking for something, and the honest answer is `400`, not a success
|
|
79
|
-
* that means less than it looks.
|
|
72
|
+
* Without it, an offered field the route does not implement is *dropped* — the
|
|
73
|
+
* caller gets a `200`, nothing changes, and nothing anywhere says so. It is the
|
|
74
|
+
* same silence `createAppRequest` above describes. A caller who sends a field
|
|
75
|
+
* this route does not do is asking for something, and the honest answer is
|
|
76
|
+
* `400`, not a success that means less than it looks.
|
|
80
77
|
*
|
|
81
|
-
* Two fields
|
|
82
|
-
* were on this shape and neither has a successor here.
|
|
78
|
+
* Two fields are absent and worth naming, because neither has a successor here.
|
|
83
79
|
* `accepts_dynamic_clients` gated app-level OAuth dynamic client registration,
|
|
84
|
-
* which
|
|
85
|
-
* only for MCP. `group_id` named
|
|
86
|
-
*
|
|
80
|
+
* which no longer exists: apps use the JSON client-auth API, and OAuth 2.1
|
|
81
|
+
* remains only for MCP. `group_id` named a group that owned the app; who may
|
|
82
|
+
* log into an app is the app's own user list.
|
|
87
83
|
*
|
|
88
84
|
* The route keeps its own check as belt-and-braces; a schema and a handler
|
|
89
85
|
* agreeing is not two policies, it is one policy stated where each half can
|
|
@@ -96,9 +92,9 @@ export declare const updateAppRequest: z.ZodObject<{
|
|
|
96
92
|
}, z.core.$strict>;
|
|
97
93
|
export type UpdateAppRequest = z.infer<typeof updateAppRequest>;
|
|
98
94
|
/**
|
|
99
|
-
* A server key carries full app rights for server-side code
|
|
100
|
-
*
|
|
101
|
-
*
|
|
95
|
+
* A server key carries full app rights for server-side code — never for
|
|
96
|
+
* clients. Same handling as the robot token: the value is returned exactly once
|
|
97
|
+
* and only its hash is stored.
|
|
102
98
|
*/
|
|
103
99
|
export declare const serverKeyToken: z.ZodString;
|
|
104
100
|
export declare const serverKey: z.ZodObject<{
|
|
@@ -133,7 +129,7 @@ export declare const createServerKeyResponse: z.ZodObject<{
|
|
|
133
129
|
export type CreateServerKeyResponse = z.infer<typeof createServerKeyResponse>;
|
|
134
130
|
/**
|
|
135
131
|
* Every app starts with `observe` and `operate`; custom roles are allowed
|
|
136
|
-
*
|
|
132
|
+
* too. `builtin` marks the two starting roles — they may be
|
|
137
133
|
* edited like any other, the flag exists so the console can explain where
|
|
138
134
|
* they came from.
|
|
139
135
|
*/
|
|
@@ -156,7 +152,7 @@ export declare const roleListResponse: z.ZodObject<{
|
|
|
156
152
|
export type RoleListResponse = z.infer<typeof roleListResponse>;
|
|
157
153
|
/**
|
|
158
154
|
* The rights matrix of one role: which slugs of which robot it may use, plus
|
|
159
|
-
* the capabilities roles also govern
|
|
155
|
+
* the capabilities roles also govern. `capabilities`' own doc comment
|
|
160
156
|
* below says which of them are enforced today and which is still a switch
|
|
161
157
|
* that changes nothing.
|
|
162
158
|
*/
|
package/dist/apps.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { slug } from './common.js';
|
|
4
4
|
/**
|
|
5
|
-
* Apps, roles and rights
|
|
5
|
+
* Apps, roles and rights.
|
|
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
|
|
@@ -33,7 +33,7 @@ export const app = z.object({
|
|
|
33
33
|
identifier: appIdentifier.meta({
|
|
34
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`.',
|
|
35
35
|
}),
|
|
36
|
-
/** Robots are referenced individually; tags never grant rights
|
|
36
|
+
/** Robots are referenced individually; tags never grant rights. */
|
|
37
37
|
robot_ids: z.array(z.uuid()).meta({
|
|
38
38
|
description: 'The robots this app may reach, each referenced individually. Tags never grant rights, and a robot absent from this list is invisible to the app whatever a role grants.',
|
|
39
39
|
}),
|
|
@@ -79,12 +79,11 @@ export const appListResponse = z.object({
|
|
|
79
79
|
}),
|
|
80
80
|
});
|
|
81
81
|
/**
|
|
82
|
-
* **`robot_ids` is accepted here, and `.strict()` catches everything else
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* definition of a shape that reads as though it does something it does not.
|
|
82
|
+
* **`robot_ids` is accepted here, and `.strict()` catches everything else.**
|
|
83
|
+
* A create shape carrying `name` and `identifier` only would let zod strip an
|
|
84
|
+
* offered `robot_ids`, so a caller creating an app *with* robots gets a `201`
|
|
85
|
+
* and an app with none — a shape that reads as though it does something it does
|
|
86
|
+
* not.
|
|
88
87
|
*
|
|
89
88
|
* Both halves matter and neither alone is enough. Accepting `robot_ids` is
|
|
90
89
|
* right because attaching robots at creation is the obvious operation and the
|
|
@@ -105,21 +104,17 @@ export const createAppRequest = z.object({
|
|
|
105
104
|
/**
|
|
106
105
|
* **`.strict()` is what makes an absent field mean something here.**
|
|
107
106
|
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* been carried one shape over. A caller who sends a field this route does not
|
|
114
|
-
* do is asking for something, and the honest answer is `400`, not a success
|
|
115
|
-
* that means less than it looks.
|
|
107
|
+
* Without it, an offered field the route does not implement is *dropped* — the
|
|
108
|
+
* caller gets a `200`, nothing changes, and nothing anywhere says so. It is the
|
|
109
|
+
* same silence `createAppRequest` above describes. A caller who sends a field
|
|
110
|
+
* this route does not do is asking for something, and the honest answer is
|
|
111
|
+
* `400`, not a success that means less than it looks.
|
|
116
112
|
*
|
|
117
|
-
* Two fields
|
|
118
|
-
* were on this shape and neither has a successor here.
|
|
113
|
+
* Two fields are absent and worth naming, because neither has a successor here.
|
|
119
114
|
* `accepts_dynamic_clients` gated app-level OAuth dynamic client registration,
|
|
120
|
-
* which
|
|
121
|
-
* only for MCP. `group_id` named
|
|
122
|
-
*
|
|
115
|
+
* which no longer exists: apps use the JSON client-auth API, and OAuth 2.1
|
|
116
|
+
* remains only for MCP. `group_id` named a group that owned the app; who may
|
|
117
|
+
* log into an app is the app's own user list.
|
|
123
118
|
*
|
|
124
119
|
* The route keeps its own check as belt-and-braces; a schema and a handler
|
|
125
120
|
* agreeing is not two policies, it is one policy stated where each half can
|
|
@@ -129,7 +124,8 @@ export const updateAppRequest = z.object({
|
|
|
129
124
|
name: z.string().min(1).max(120).optional(),
|
|
130
125
|
robot_ids: z.array(z.uuid()).optional(),
|
|
131
126
|
/**
|
|
132
|
-
* `app.default_role_id`'s write half — an app *setting*, which is where
|
|
127
|
+
* `app.default_role_id`'s write half — an app *setting*, which is where the
|
|
128
|
+
* two-identity-space model
|
|
133
129
|
* put the default role, so it belongs on the app's own PATCH and not on a
|
|
134
130
|
* route of its own.
|
|
135
131
|
*
|
|
@@ -142,9 +138,9 @@ export const updateAppRequest = z.object({
|
|
|
142
138
|
default_role_id: z.uuid().nullable().optional(),
|
|
143
139
|
}).strict();
|
|
144
140
|
/**
|
|
145
|
-
* A server key carries full app rights for server-side code
|
|
146
|
-
*
|
|
147
|
-
*
|
|
141
|
+
* A server key carries full app rights for server-side code — never for
|
|
142
|
+
* clients. Same handling as the robot token: the value is returned exactly once
|
|
143
|
+
* and only its hash is stored.
|
|
148
144
|
*/
|
|
149
145
|
export const serverKeyToken = z.string().regex(/^flk_[0-9a-f]{32}$/);
|
|
150
146
|
export const serverKey = z.object({
|
|
@@ -177,7 +173,7 @@ export const createServerKeyResponse = z.object({
|
|
|
177
173
|
});
|
|
178
174
|
/**
|
|
179
175
|
* Every app starts with `observe` and `operate`; custom roles are allowed
|
|
180
|
-
*
|
|
176
|
+
* too. `builtin` marks the two starting roles — they may be
|
|
181
177
|
* edited like any other, the flag exists so the console can explain where
|
|
182
178
|
* they came from.
|
|
183
179
|
*/
|
|
@@ -203,23 +199,20 @@ export const roleListResponse = z.object({
|
|
|
203
199
|
});
|
|
204
200
|
/**
|
|
205
201
|
* The rights matrix of one role: which slugs of which robot it may use, plus
|
|
206
|
-
* the capabilities roles also govern
|
|
202
|
+
* the capabilities roles also govern. `capabilities`' own doc comment
|
|
207
203
|
* below says which of them are enforced today and which is still a switch
|
|
208
204
|
* that changes nothing.
|
|
209
205
|
*/
|
|
210
206
|
export const rolePermissions = z.object({
|
|
211
207
|
role_id: z.uuid(),
|
|
212
208
|
/**
|
|
213
|
-
* **A slug is unique per robot across ALL
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* slug of a robot with its kind, so the console's matrix can offer them —
|
|
221
|
-
* today it enumerates datapoints only, which is the seam that would
|
|
222
|
-
* otherwise force a rebuild.
|
|
209
|
+
* **A slug is unique per robot across ALL exposure kinds** — one namespace,
|
|
210
|
+
* not one per kind — and the cloud's configuration validation enforces that
|
|
211
|
+
* with a kind-agnostic collection pass. That is why this list carries slugs
|
|
212
|
+
* and not (kind, slug) pairs: a grant means the same thing whichever kind the
|
|
213
|
+
* slug turns out to name. `GET /api/robots/:id/exposures` is the companion
|
|
214
|
+
* read that lists every grantable slug of a robot **with** its kind, so a
|
|
215
|
+
* rights matrix can offer them.
|
|
223
216
|
*/
|
|
224
217
|
grants: z.array(z.object({
|
|
225
218
|
robot_id: z.uuid(),
|
|
@@ -228,25 +221,22 @@ export const rolePermissions = z.object({
|
|
|
228
221
|
/**
|
|
229
222
|
* App-wide abilities a role grants, as opposed to per-slug grants above.
|
|
230
223
|
*
|
|
231
|
-
* **A capability here is a promise, and one of them is still not kept.**
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* is: it is the only place that says a switch in the console may change
|
|
238
|
-
* nothing, and it is how the next unkept capability gets caught.
|
|
224
|
+
* **A capability here is a promise, and one of them is still not kept.** A
|
|
225
|
+
* capability with no route, no SDK method and no realtime frame behind it can
|
|
226
|
+
* be switched on while nothing changes, which is worse than its absence: the
|
|
227
|
+
* developer believes they granted something. **This paragraph stays**
|
|
228
|
+
* whatever the current tally is: it is the only place that says a switch may
|
|
229
|
+
* change nothing, and it is how the next unkept capability gets caught.
|
|
239
230
|
*
|
|
240
|
-
* `assets`
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
231
|
+
* `assets` gates the asset store, which is not covered by `grants` because
|
|
232
|
+
* **assets are not slugs** — and it is its own decision rather than a side
|
|
233
|
+
* effect of reaching the robot, because a mesh set gives away the machine's
|
|
234
|
+
* build.
|
|
244
235
|
*
|
|
245
|
-
* **`action_history` is kept
|
|
236
|
+
* **`action_history` is kept.** It gates
|
|
246
237
|
* `GET /api/robots/:id/jobs/history` — an end user whose role lacks it is
|
|
247
238
|
* refused `403 capability_required`, naming the capability so the developer
|
|
248
|
-
* knows which switch is off.
|
|
249
|
-
* recorded what had run; `jobRun` and `job_runs` are that record.
|
|
239
|
+
* knows which switch is off.
|
|
250
240
|
*
|
|
251
241
|
* **What granting it discloses.** A `jobRun` names the actor who invoked
|
|
252
242
|
* it, and `jobActor.label` is an email — so an end user holding this
|