@fleetless/contracts 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (287) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +17 -0
  4. package/README.md +88 -0
  5. package/artifacts/constants.json +24 -0
  6. package/artifacts/openapi.json +17219 -0
  7. package/artifacts/routes.json +4605 -0
  8. package/artifacts/schema/accept-team-invite-request.schema.json +22 -0
  9. package/artifacts/schema/action-config.schema.json +198 -0
  10. package/artifacts/schema/alert-list-response.schema.json +172 -0
  11. package/artifacts/schema/api-error.schema.json +20 -0
  12. package/artifacts/schema/app-auth-config.schema.json +106 -0
  13. package/artifacts/schema/app-invitation-list-response.schema.json +57 -0
  14. package/artifacts/schema/app-invitation.schema.json +69 -0
  15. package/artifacts/schema/app-list-response.schema.json +82 -0
  16. package/artifacts/schema/app-mail-template-list-response.schema.json +68 -0
  17. package/artifacts/schema/app-mail-template.schema.json +54 -0
  18. package/artifacts/schema/app-oidc-provider-list-response.schema.json +93 -0
  19. package/artifacts/schema/app-oidc-provider.schema.json +80 -0
  20. package/artifacts/schema/app-user-list-response.schema.json +111 -0
  21. package/artifacts/schema/app-user.schema.json +98 -0
  22. package/artifacts/schema/app.schema.json +69 -0
  23. package/artifacts/schema/apply-error.schema.json +41 -0
  24. package/artifacts/schema/asset-list-response.schema.json +288 -0
  25. package/artifacts/schema/asset-sync-request.schema.json +17 -0
  26. package/artifacts/schema/asset-sync-response.schema.json +16 -0
  27. package/artifacts/schema/asset-sync-status.schema.json +136 -0
  28. package/artifacts/schema/asset.schema.json +68 -0
  29. package/artifacts/schema/audit-actor.schema.json +32 -0
  30. package/artifacts/schema/audit-event.schema.json +119 -0
  31. package/artifacts/schema/audit-list-response.schema.json +144 -0
  32. package/artifacts/schema/audit-query.schema.json +79 -0
  33. package/artifacts/schema/auth-error.schema.json +23 -0
  34. package/artifacts/schema/auth-me-response.schema.json +99 -0
  35. package/artifacts/schema/auth-ok.schema.json +115 -0
  36. package/artifacts/schema/authorization-server-metadata.schema.json +80 -0
  37. package/artifacts/schema/bridge-asset-progress.schema.json +99 -0
  38. package/artifacts/schema/bridge-assets-available.schema.json +25 -0
  39. package/artifacts/schema/bridge-camera-state.schema.json +78 -0
  40. package/artifacts/schema/bridge-config-applied.schema.json +67 -0
  41. package/artifacts/schema/bridge-hello.schema.json +65 -0
  42. package/artifacts/schema/bridge-introspect.schema.json +114 -0
  43. package/artifacts/schema/bridge-job-lost.schema.json +22 -0
  44. package/artifacts/schema/bridge-job-update.schema.json +100 -0
  45. package/artifacts/schema/bridge-pong.schema.json +19 -0
  46. package/artifacts/schema/bridge-pressure.schema.json +292 -0
  47. package/artifacts/schema/bridge-state.schema.json +24 -0
  48. package/artifacts/schema/bridge-type-definitions.schema.json +169 -0
  49. package/artifacts/schema/busy-details.schema.json +115 -0
  50. package/artifacts/schema/camera-descriptor.schema.json +45 -0
  51. package/artifacts/schema/camera-list-response.schema.json +58 -0
  52. package/artifacts/schema/camera-source.schema.json +240 -0
  53. package/artifacts/schema/cancel-request.schema.json +20 -0
  54. package/artifacts/schema/client-accept-invitation-request.schema.json +35 -0
  55. package/artifacts/schema/client-auth.schema.json +18 -0
  56. package/artifacts/schema/client-cancel.schema.json +45 -0
  57. package/artifacts/schema/client-identity.schema.json +103 -0
  58. package/artifacts/schema/client-invoke.schema.json +45 -0
  59. package/artifacts/schema/client-login-request.schema.json +29 -0
  60. package/artifacts/schema/client-logout-request.schema.json +14 -0
  61. package/artifacts/schema/client-mcp-interaction-decision-response.schema.json +15 -0
  62. package/artifacts/schema/client-mcp-interaction.schema.json +59 -0
  63. package/artifacts/schema/client-oidc-callback-query.schema.json +28 -0
  64. package/artifacts/schema/client-oidc-exchange-request.schema.json +21 -0
  65. package/artifacts/schema/client-oidc-start-query.schema.json +36 -0
  66. package/artifacts/schema/client-password-reset-confirm-request.schema.json +22 -0
  67. package/artifacts/schema/client-password-reset-request.schema.json +24 -0
  68. package/artifacts/schema/client-provider-list-query.schema.json +17 -0
  69. package/artifacts/schema/client-provider-list-response.schema.json +34 -0
  70. package/artifacts/schema/client-publish.schema.json +40 -0
  71. package/artifacts/schema/client-refresh-request.schema.json +14 -0
  72. package/artifacts/schema/client-register-request.schema.json +44 -0
  73. package/artifacts/schema/client-resend-verification-request.schema.json +24 -0
  74. package/artifacts/schema/client-subscribe.schema.json +43 -0
  75. package/artifacts/schema/client-unsubscribe.schema.json +26 -0
  76. package/artifacts/schema/client-verify-email-request.schema.json +15 -0
  77. package/artifacts/schema/cloud-asset-request.schema.json +37 -0
  78. package/artifacts/schema/cloud-camera-start.schema.json +41 -0
  79. package/artifacts/schema/cloud-camera-stop.schema.json +26 -0
  80. package/artifacts/schema/cloud-cancel.schema.json +33 -0
  81. package/artifacts/schema/cloud-config.schema.json +1635 -0
  82. package/artifacts/schema/cloud-hello-error.schema.json +23 -0
  83. package/artifacts/schema/cloud-hello-ok.schema.json +19 -0
  84. package/artifacts/schema/cloud-introspect-request.schema.json +19 -0
  85. package/artifacts/schema/cloud-invoke.schema.json +40 -0
  86. package/artifacts/schema/cloud-ping.schema.json +19 -0
  87. package/artifacts/schema/cloud-publish.schema.json +28 -0
  88. package/artifacts/schema/cloud-type-request.schema.json +30 -0
  89. package/artifacts/schema/command-result.schema.json +175 -0
  90. package/artifacts/schema/config-draft-response.schema.json +1695 -0
  91. package/artifacts/schema/config-state.schema.json +124 -0
  92. package/artifacts/schema/config-version-response.schema.json +1641 -0
  93. package/artifacts/schema/config-versions-response.schema.json +33 -0
  94. package/artifacts/schema/create-app-invitation-request.schema.json +40 -0
  95. package/artifacts/schema/create-app-oidc-provider-request.schema.json +70 -0
  96. package/artifacts/schema/create-app-request.schema.json +30 -0
  97. package/artifacts/schema/create-app-user-request.schema.json +42 -0
  98. package/artifacts/schema/create-robot-request.schema.json +14 -0
  99. package/artifacts/schema/create-robot-response.schema.json +44 -0
  100. package/artifacts/schema/create-server-key-response.schema.json +65 -0
  101. package/artifacts/schema/create-team-invite-request.schema.json +43 -0
  102. package/artifacts/schema/datapoint-alert-row.schema.json +160 -0
  103. package/artifacts/schema/datapoint-config.schema.json +366 -0
  104. package/artifacts/schema/datapoint-display.schema.json +31 -0
  105. package/artifacts/schema/datapoint-event.schema.json +34 -0
  106. package/artifacts/schema/datapoint-frame.schema.json +28 -0
  107. package/artifacts/schema/datapoint-list-response.schema.json +61 -0
  108. package/artifacts/schema/datapoint-value.schema.json +28 -0
  109. package/artifacts/schema/developer-login-request.schema.json +19 -0
  110. package/artifacts/schema/dynamic-client-registration-request.schema.json +60 -0
  111. package/artifacts/schema/dynamic-client-registration-response.schema.json +68 -0
  112. package/artifacts/schema/error-frame.schema.json +23 -0
  113. package/artifacts/schema/exposure-counts.schema.json +39 -0
  114. package/artifacts/schema/exposure-list-response.schema.json +43 -0
  115. package/artifacts/schema/fetch-types-request.schema.json +19 -0
  116. package/artifacts/schema/fetch-types-response.schema.json +163 -0
  117. package/artifacts/schema/fleetless-user-list-response.schema.json +73 -0
  118. package/artifacts/schema/fleetless-user.schema.json +60 -0
  119. package/artifacts/schema/history-buckets-response.schema.json +79 -0
  120. package/artifacts/schema/history-query.schema.json +58 -0
  121. package/artifacts/schema/history-response.schema.json +150 -0
  122. package/artifacts/schema/history-samples-response.schema.json +68 -0
  123. package/artifacts/schema/introspection-response.schema.json +118 -0
  124. package/artifacts/schema/invoke-or-service-response.schema.json +141 -0
  125. package/artifacts/schema/invoke-request.schema.json +23 -0
  126. package/artifacts/schema/invoke-response.schema.json +125 -0
  127. package/artifacts/schema/job-actor.schema.json +34 -0
  128. package/artifacts/schema/job-event.schema.json +158 -0
  129. package/artifacts/schema/job-response.schema.json +123 -0
  130. package/artifacts/schema/job-run-list-response.schema.json +222 -0
  131. package/artifacts/schema/job-run-query.schema.json +95 -0
  132. package/artifacts/schema/job-run-summary-query.schema.json +23 -0
  133. package/artifacts/schema/job-run-summary.schema.json +33 -0
  134. package/artifacts/schema/job-run.schema.json +195 -0
  135. package/artifacts/schema/job-state.schema.json +11 -0
  136. package/artifacts/schema/job.schema.json +106 -0
  137. package/artifacts/schema/latency-bucket.schema.json +63 -0
  138. package/artifacts/schema/live-session-response.schema.json +41 -0
  139. package/artifacts/schema/mail-outcome.schema.json +20 -0
  140. package/artifacts/schema/mail-template-preview-request.schema.json +35 -0
  141. package/artifacts/schema/mail-template-preview-response.schema.json +31 -0
  142. package/artifacts/schema/mail-template-problem-details.schema.json +24 -0
  143. package/artifacts/schema/mcp-consent-grant-list-response.schema.json +52 -0
  144. package/artifacts/schema/mcp-consent-grant.schema.json +39 -0
  145. package/artifacts/schema/mcp-robot-datasheet.schema.json +115 -0
  146. package/artifacts/schema/mcp-role-preview-response.schema.json +134 -0
  147. package/artifacts/schema/missing-asset-query.schema.json +11 -0
  148. package/artifacts/schema/oauth-authorize-query.schema.json +47 -0
  149. package/artifacts/schema/oauth-redirect-response.schema.json +15 -0
  150. package/artifacts/schema/oauth-token-request.schema.json +47 -0
  151. package/artifacts/schema/oauth-token-response.schema.json +38 -0
  152. package/artifacts/schema/org-alerts-query.schema.json +15 -0
  153. package/artifacts/schema/org-event-dropped.schema.json +26 -0
  154. package/artifacts/schema/org-event-replay.schema.json +97 -0
  155. package/artifacts/schema/org-event-subscribe.schema.json +14 -0
  156. package/artifacts/schema/org-event-unsubscribe.schema.json +14 -0
  157. package/artifacts/schema/org-event.schema.json +75 -0
  158. package/artifacts/schema/org-firing-alerts-response.schema.json +178 -0
  159. package/artifacts/schema/org-health-query.schema.json +13 -0
  160. package/artifacts/schema/org-latency-query.schema.json +42 -0
  161. package/artifacts/schema/org-latency-response.schema.json +124 -0
  162. package/artifacts/schema/org-quota-usage-counts.schema.json +42 -0
  163. package/artifacts/schema/org-quota-usage.schema.json +102 -0
  164. package/artifacts/schema/org-quotas.schema.json +51 -0
  165. package/artifacts/schema/org-usage-query.schema.json +19 -0
  166. package/artifacts/schema/org-usage-response.schema.json +77 -0
  167. package/artifacts/schema/org.schema.json +30 -0
  168. package/artifacts/schema/parameter-invalid-details.schema.json +37 -0
  169. package/artifacts/schema/parameter-spec.schema.json +120 -0
  170. package/artifacts/schema/parameter-violation.schema.json +24 -0
  171. package/artifacts/schema/password-change-request.schema.json +21 -0
  172. package/artifacts/schema/password-reset-confirm.schema.json +19 -0
  173. package/artifacts/schema/password-reset-request.schema.json +14 -0
  174. package/artifacts/schema/patch-app-oidc-provider-request.schema.json +50 -0
  175. package/artifacts/schema/patch-app-user-request.schema.json +34 -0
  176. package/artifacts/schema/patch-auth-me-request.schema.json +22 -0
  177. package/artifacts/schema/patch-fleetless-user-request.schema.json +20 -0
  178. package/artifacts/schema/patch-org-request.schema.json +15 -0
  179. package/artifacts/schema/patch-org-response.schema.json +40 -0
  180. package/artifacts/schema/patch-robot-request.schema.json +15 -0
  181. package/artifacts/schema/patch-robot-response.schema.json +40 -0
  182. package/artifacts/schema/pending-team-invite-list-response.schema.json +52 -0
  183. package/artifacts/schema/pending-team-invite.schema.json +39 -0
  184. package/artifacts/schema/protected-resource-metadata.schema.json +41 -0
  185. package/artifacts/schema/publish-config-response.schema.json +21 -0
  186. package/artifacts/schema/publish-request.schema.json +17 -0
  187. package/artifacts/schema/publisher-config.schema.json +285 -0
  188. package/artifacts/schema/put-app-auth-config-request.schema.json +93 -0
  189. package/artifacts/schema/put-app-mail-template-request.schema.json +35 -0
  190. package/artifacts/schema/put-config-draft-request.schema.json +13 -0
  191. package/artifacts/schema/put-datapoint-display-request.schema.json +31 -0
  192. package/artifacts/schema/put-robot-details-request.schema.json +41 -0
  193. package/artifacts/schema/put-robot-details-response.schema.json +43 -0
  194. package/artifacts/schema/rate-limit-details.schema.json +15 -0
  195. package/artifacts/schema/refresh-request.schema.json +13 -0
  196. package/artifacts/schema/release-live-query.schema.json +13 -0
  197. package/artifacts/schema/rename-slug-request.schema.json +23 -0
  198. package/artifacts/schema/rename-slug-response.schema.json +24 -0
  199. package/artifacts/schema/resource-health-event.schema.json +72 -0
  200. package/artifacts/schema/resource-health-list-response.schema.json +80 -0
  201. package/artifacts/schema/resource-health-state.schema.json +68 -0
  202. package/artifacts/schema/robot-config-doc.schema.json +1616 -0
  203. package/artifacts/schema/robot-delete-query.schema.json +12 -0
  204. package/artifacts/schema/robot-deletion-summary.schema.json +63 -0
  205. package/artifacts/schema/robot-detail-response.schema.json +262 -0
  206. package/artifacts/schema/robot-details-doc.schema.json +33 -0
  207. package/artifacts/schema/robot-jobs-response.schema.json +119 -0
  208. package/artifacts/schema/robot-latency-series.schema.json +81 -0
  209. package/artifacts/schema/robot-list-item.schema.json +94 -0
  210. package/artifacts/schema/robot-list-response.schema.json +106 -0
  211. package/artifacts/schema/robot.schema.json +30 -0
  212. package/artifacts/schema/role-list-response.schema.json +48 -0
  213. package/artifacts/schema/role-permissions.schema.json +61 -0
  214. package/artifacts/schema/role.schema.json +35 -0
  215. package/artifacts/schema/ros-graph.schema.json +99 -0
  216. package/artifacts/schema/server-key-list-response.schema.json +64 -0
  217. package/artifacts/schema/server-key.schema.json +51 -0
  218. package/artifacts/schema/service-call-response.schema.json +13 -0
  219. package/artifacts/schema/service-config.schema.json +198 -0
  220. package/artifacts/schema/session-tokens.schema.json +28 -0
  221. package/artifacts/schema/sign-up-request.schema.json +26 -0
  222. package/artifacts/schema/sign-up-response.schema.json +127 -0
  223. package/artifacts/schema/slug-usage-response.schema.json +32 -0
  224. package/artifacts/schema/snapshot-header.schema.json +44 -0
  225. package/artifacts/schema/snapshot-meta-response.schema.json +85 -0
  226. package/artifacts/schema/subscribe-error.schema.json +31 -0
  227. package/artifacts/schema/team-invite.schema.json +57 -0
  228. package/artifacts/schema/tier-change-request.schema.json +17 -0
  229. package/artifacts/schema/type-definition.schema.json +144 -0
  230. package/artifacts/schema/types-response.schema.json +156 -0
  231. package/artifacts/schema/update-app-request.schema.json +32 -0
  232. package/artifacts/schema/urdf-completeness.schema.json +50 -0
  233. package/artifacts/schema/validation-issue.schema.json +43 -0
  234. package/artifacts/schema/waitlist-request.schema.json +15 -0
  235. package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +102 -0
  236. package/artifacts/schema-outgoing/bridge-assets-available.schema.json +26 -0
  237. package/artifacts/schema-outgoing/bridge-camera-state.schema.json +80 -0
  238. package/artifacts/schema-outgoing/bridge-config-applied.schema.json +69 -0
  239. package/artifacts/schema-outgoing/bridge-hello.schema.json +68 -0
  240. package/artifacts/schema-outgoing/bridge-introspect.schema.json +119 -0
  241. package/artifacts/schema-outgoing/bridge-job-lost.schema.json +23 -0
  242. package/artifacts/schema-outgoing/bridge-job-update.schema.json +102 -0
  243. package/artifacts/schema-outgoing/bridge-pong.schema.json +20 -0
  244. package/artifacts/schema-outgoing/bridge-type-definitions.schema.json +174 -0
  245. package/artifacts/schema-outgoing/datapoint-frame.schema.json +29 -0
  246. package/artifacts/schema-outgoing/snapshot-header.schema.json +45 -0
  247. package/dist/alerts.d.ts +255 -0
  248. package/dist/alerts.js +193 -0
  249. package/dist/app-users.d.ts +606 -0
  250. package/dist/app-users.js +696 -0
  251. package/dist/apps.d.ts +175 -0
  252. package/dist/apps.js +267 -0
  253. package/dist/assets.d.ts +434 -0
  254. package/dist/assets.js +546 -0
  255. package/dist/audit.d.ts +129 -0
  256. package/dist/audit.js +238 -0
  257. package/dist/client-auth.d.ts +409 -0
  258. package/dist/client-auth.js +487 -0
  259. package/dist/common.d.ts +186 -0
  260. package/dist/common.js +199 -0
  261. package/dist/config-issues.d.ts +175 -0
  262. package/dist/config-issues.js +339 -0
  263. package/dist/config.d.ts +862 -0
  264. package/dist/config.js +1988 -0
  265. package/dist/errors.d.ts +52 -0
  266. package/dist/errors.js +786 -0
  267. package/dist/identity.d.ts +549 -0
  268. package/dist/identity.js +503 -0
  269. package/dist/index.d.ts +51 -0
  270. package/dist/index.js +51 -0
  271. package/dist/introspection.d.ts +99 -0
  272. package/dist/introspection.js +97 -0
  273. package/dist/jobs.d.ts +334 -0
  274. package/dist/jobs.js +345 -0
  275. package/dist/mcp.d.ts +239 -0
  276. package/dist/mcp.js +153 -0
  277. package/dist/oauth.d.ts +344 -0
  278. package/dist/oauth.js +488 -0
  279. package/dist/protocol.d.ts +781 -0
  280. package/dist/protocol.js +715 -0
  281. package/dist/realtime.d.ts +494 -0
  282. package/dist/realtime.js +512 -0
  283. package/dist/rest.d.ts +1989 -0
  284. package/dist/rest.js +1963 -0
  285. package/dist/routes.d.ts +94 -0
  286. package/dist/routes.js +2298 -0
  287. package/package.json +61 -0
@@ -0,0 +1,487 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { z } from 'zod';
3
+ import { appIdentifier } from './apps.js';
4
+ import { APP_USER_DISPLAY_NAME_MAX, providerSlug } from './app-users.js';
5
+ import { password } from './identity.js';
6
+ /**
7
+ * **The client auth API: the whole of what an app user's browser talks to**
8
+ * (spec `2026-09-05-app-user-auth`, §4).
9
+ *
10
+ * Fleetless shows an app user **no page** (D2). The developer's own UI owns
11
+ * every screen — login, registration, verification, invitation acceptance,
12
+ * password reset, the provider buttons, the MCP consent — and calls these
13
+ * routes as JSON. The hosted, app-branded login and consent pages this file
14
+ * used to describe are deleted.
15
+ *
16
+ * Everything here is public: `app_identifier` travels in the body (in the
17
+ * query for a GET), CORS is answered only for the app's `allowed_origins`, and
18
+ * the whole family is rate-limited per app, address and IP.
19
+ *
20
+ * **Two kinds of caller reach the authenticated half**, and both use
21
+ * `Authorization: Bearer`:
22
+ *
23
+ * - an **app user**, with the JWT access token issued here;
24
+ * - a **server key** (`flk_…`), for server-side code, carrying full app rights.
25
+ *
26
+ * The cloud tells them apart by shape — a `flk_` prefix is a server key,
27
+ * anything else is parsed as a JWT. That rule is written down once, here, so
28
+ * the SDK and the cloud cannot drift into disagreeing about it.
29
+ *
30
+ * **The enumeration discipline is the design's, not a preference** (§4):
31
+ * `register`, `resend-verification` and `password/reset` answer `202` for every
32
+ * policy-allowed request whether or not the address exists, and `login` answers
33
+ * the identical `invalid_credentials` for a wrong password, a `blocked` account
34
+ * and a `pending_verification` one. Policy refusals are honest —
35
+ * `registration_closed` and `domain_not_allowed` say what they are, because
36
+ * neither reveals whether a *person* exists.
37
+ */
38
+ /* ------------------------------------------------------ password login -- */
39
+ export const clientLoginRequest = z.object({
40
+ app_identifier: appIdentifier.meta({
41
+ 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.',
42
+ }),
43
+ email: z.email().meta({
44
+ 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.',
45
+ }),
46
+ password: z.string().min(1).meta({
47
+ description: 'The app user\'s password. A wrong pair is refused without saying which half was wrong — and a `blocked` or not-yet-verified account is refused identically, so a failed login is not an account-enumeration oracle in any of its three forms.',
48
+ }),
49
+ });
50
+ export const clientRefreshRequest = z.object({
51
+ refresh_token: z.string().min(1).meta({
52
+ description: 'The refresh token from the last login or refresh. Refresh tokens rotate on every use, so the value sent here is spent — keep the one that comes back, and presenting a spent one is treated as theft and ends the whole family.',
53
+ }),
54
+ });
55
+ /**
56
+ * Logging out revokes the whole token family server-side. Without this, a
57
+ * refresh token stolen before the user pressed "log out" keeps working —
58
+ * clearing a client-side store is a UI gesture, not a revocation.
59
+ *
60
+ * **The route answers `204` and has no response shape.** It used to answer a
61
+ * `clientLogoutResponse` reporting what was left of the session at the identity
62
+ * provider — RP-initiated logout, an `end_session_endpoint` to redirect to,
63
+ * four ways of saying "we cannot end that session". That whole apparatus
64
+ * belonged to the hosted login flow, where Fleetless owned the browser. It does
65
+ * not own it any more: the developer's app does, and an app that wants to end
66
+ * a provider session redirects there itself, knowing its own provider, which
67
+ * Fleetless never did better than it. Listed as a breaking change rather than
68
+ * quietly kept as a field nobody fills.
69
+ */
70
+ export const clientLogoutRequest = z.object({
71
+ refresh_token: z.string().min(1).meta({
72
+ 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.',
73
+ }),
74
+ });
75
+ /* ---------------------------------------------- registration and mails -- */
76
+ /**
77
+ * **Self-registration** (D6) — and the account it creates cannot log in yet.
78
+ *
79
+ * `register` writes the user as `pending_verification` and mails the app's
80
+ * `verify_url`. Without that step the domain whitelist would prove nothing:
81
+ * anybody could claim any address at an allowed domain and be `active`
82
+ * immediately.
83
+ *
84
+ * **The answer is `202` for every policy-allowed request**, whether the address
85
+ * was new or already known — a mail goes out only in the first case, and a
86
+ * `register` that finds the address on an account still waiting to verify
87
+ * replaces that account's password and mails a fresh link, so the mailbox's own
88
+ * owner always wins over whoever typed their address first. A `202` that
89
+ * depended on existence would be the enumeration oracle the whole family is
90
+ * built to avoid. The refusals it *does* make are honest, because none is about
91
+ * a person: `403 registration_closed` when the app has self-registration off,
92
+ * `403 domain_not_allowed` when the address is outside `allowed_domains`, and
93
+ * `404 not_found` for an app identifier no app carries.
94
+ */
95
+ export const clientRegisterRequest = z
96
+ .object({
97
+ app_identifier: appIdentifier.meta({
98
+ description: 'The app to register with. An identifier no app carries is `404 not_found` — an identifier is public, so naming it is no disclosure, and collapsing it into `registration_closed` sent a developer who mistyped their own identifier hunting a configuration bug that was not there. The **address** is never the subject of a refusal.',
99
+ }),
100
+ email: z.email().meta({
101
+ description: 'The address to register. Unique per app, case-insensitively. An address this app already knows still answers `202`, without a mail — the answer may not say whether an account exists.',
102
+ }),
103
+ password: password.meta({
104
+ description: 'The password for the new account. At least 12 characters; length only, because a rule a user cannot predict is a rule they work around.',
105
+ }),
106
+ display_name: z.string().min(1).max(APP_USER_DISPLAY_NAME_MAX).nullable().optional().meta({
107
+ description: 'An optional human name for the account. The developer\'s own UI decides whether to ask for it.',
108
+ }),
109
+ })
110
+ .strict();
111
+ /** Spending the verification token: the account becomes `active` and the answer is a session, so the person is not asked to log in immediately after proving they can read the mail. */
112
+ export const clientVerifyEmailRequest = z
113
+ .object({
114
+ token: z.string().min(1).meta({
115
+ description: 'The opaque token from the verification link, valid 24 hours. Unknown, expired and already-spent all answer `410 token_spent` — telling them apart would say whether a token ever existed.',
116
+ }),
117
+ })
118
+ .strict();
119
+ /** Asking for the verification mail again. **Always `202`**, for the reason `register` is: an answer that depended on the address existing would be the oracle by another door. */
120
+ export const clientResendVerificationRequest = z
121
+ .object({
122
+ app_identifier: appIdentifier.meta({ description: 'The app the address belongs to.' }),
123
+ email: z.email().meta({
124
+ description: 'The address to re-send to. The answer is `202` whether or not it names an account, and whether or not that account is already verified.',
125
+ }),
126
+ })
127
+ .strict();
128
+ /**
129
+ * Asking for a reset link **as an app user**.
130
+ *
131
+ * Same act as `passwordResetRequest`, different shape, because the two surfaces
132
+ * identify a person differently. A Fleetless user's address is globally unique
133
+ * and resolves alone; an app user's is unique only within their app, so the
134
+ * pair is what names them.
135
+ *
136
+ * The response is identical for a known and an unknown pair — otherwise this
137
+ * becomes the enumeration oracle the rest of the family is carefully built not
138
+ * to be. An **app identifier** no app carries is the one refusal, `404
139
+ * not_found`, because an identifier is public and an address is not.
140
+ *
141
+ * Moved here from `identity.ts`, where it sat because the client surface had no
142
+ * file of its own for it. It is an app-user shape and belongs with them.
143
+ */
144
+ export const clientPasswordResetRequest = z
145
+ .object({
146
+ app_identifier: appIdentifier.meta({ description: 'The app the address belongs to.' }),
147
+ email: z.email().meta({
148
+ description: 'The address to mail a reset link to. The answer is `202` for a known address and an unknown one alike, in status, body and timing. An app identifier no app carries is `404 not_found`; the address is never the subject of a refusal.',
149
+ }),
150
+ })
151
+ .strict();
152
+ /** Spending the reset token. **Every refresh family of that user is revoked**, because a forgotten password is one of the two states where somebody else may be holding a session. */
153
+ export const clientPasswordResetConfirmRequest = z
154
+ .object({
155
+ token: z.string().min(1).meta({
156
+ description: 'The opaque token from the reset link, valid one hour. Single-use; unknown, expired and spent all answer `410 token_spent`.',
157
+ }),
158
+ new_password: password.meta({
159
+ description: 'The replacement password. Accepting it revokes every refresh family the account holds — the answer carries a fresh pair, so the person is signed in on the device that completed the reset and nowhere else.',
160
+ }),
161
+ })
162
+ .strict();
163
+ /**
164
+ * Accepting an app invitation. Creates the account, or activates one that was
165
+ * invited before it existed, with the role the invitation fixed at creation.
166
+ *
167
+ * An invitation **always bypasses the domain whitelist**: a developer inviting
168
+ * somebody by hand has already made the decision the whitelist automates.
169
+ */
170
+ export const clientAcceptInvitationRequest = z
171
+ .object({
172
+ token: z.string().min(1).meta({
173
+ description: 'The opaque token from the invitation link, valid seven days. Unknown, expired, revoked and already-accepted all answer `410 token_spent`.',
174
+ }),
175
+ password: password.meta({ description: 'The password the new account will use.' }),
176
+ display_name: z.string().min(1).max(APP_USER_DISPLAY_NAME_MAX).nullable().optional().meta({
177
+ description: 'An optional name, overriding whatever the invitation pre-filled.',
178
+ }),
179
+ })
180
+ .strict();
181
+ /* ------------------------------------------------------ OIDC, per app -- */
182
+ /**
183
+ * **The one OIDC callback path, for every app and every provider**, declared
184
+ * once so the cloud, the console and the documentation cannot spell it
185
+ * differently.
186
+ *
187
+ * `appAuthConfig.oidc_callback_url` is this path appended to the cloud's own
188
+ * `PUBLIC_API_BASE_URL`, and that URL is what a developer registers at their
189
+ * identity provider. So the string is not an implementation detail of one
190
+ * route: it is copied out of the console into somebody else's IdP
191
+ * configuration, where a later rename would break every sign-in with no error
192
+ * anybody here can see.
193
+ *
194
+ * **This is the path, not the URL.** The cloud mints the URL from its
195
+ * canonical public base — the same rule `MCP_ENDPOINT_PATH` states — and a
196
+ * friendly alias in front of the API is not a substitute, because the
197
+ * redirect target must match the one string registered at the provider
198
+ * exactly.
199
+ *
200
+ * `OAUTH_PATHS` is the precedent, and the warning: nine paths declared once so
201
+ * two repositories could not disagree, one of which then named a route the
202
+ * cloud had deleted. What keeps this one honest is `routes.ts` — the manifest
203
+ * carries the same path, the cloud's `route-manifest.test.ts` asserts set
204
+ * equality with it, and the test beside this file asserts the two spellings
205
+ * are the one string rather than two that currently agree.
206
+ */
207
+ export const CLIENT_OIDC_CALLBACK_PATH = '/api/client/oidc/callback';
208
+ /** The query of `GET /api/client/providers` — which app's sign-in buttons to draw. */
209
+ export const clientProviderListQuery = z
210
+ .object({
211
+ app_identifier: appIdentifier.meta({ description: 'The app whose enabled providers to list.' }),
212
+ })
213
+ .meta({ description: 'The one parameter of the public provider listing.' });
214
+ /**
215
+ * What the developer's login page needs to draw its provider buttons, and
216
+ * **nothing more**. This route is public and unauthenticated: the issuer, the
217
+ * client id, the scopes and the linking policy are all management-side facts
218
+ * that would tell a stranger how the app's federation is configured.
219
+ *
220
+ * Only **enabled** providers appear. A disabled one is not a button that
221
+ * refuses; it is a button that is not there.
222
+ */
223
+ export const clientProviderListResponse = z.object({
224
+ providers: z
225
+ .array(z.object({
226
+ slug: providerSlug.meta({ description: 'The handle to put in the start URL: `GET /api/client/oidc/<slug>/start`.' }),
227
+ name: z.string().meta({ description: 'What to write on the button, as the developer configured it.' }),
228
+ }))
229
+ .meta({
230
+ description: 'The app\'s **enabled** providers, slug and display name only. An app with none answers an empty array, which is the state of an app that offers password login alone.',
231
+ }),
232
+ });
233
+ /**
234
+ * The query of `GET /api/client/oidc/:slug/start`.
235
+ *
236
+ * **The app runs its own PKCE** here, against Fleetless — a second, independent
237
+ * exchange from the one Fleetless runs against the identity provider. So the
238
+ * one-time code the callback hands back is bound to a verifier only the app's
239
+ * page holds, and a code intercepted in the redirect is worth nothing on its
240
+ * own.
241
+ *
242
+ * `redirect_uri` is validated against the app's `allowed_origins` **before
243
+ * anything else**, and a failure there never redirects: until the target is
244
+ * known-good, sending a browser to it is the attack.
245
+ */
246
+ export const clientOidcStartQuery = z.object({
247
+ app_identifier: appIdentifier.meta({ description: 'The app being signed in to.' }),
248
+ redirect_uri: z.url().max(2000).meta({
249
+ description: 'Where to send the browser when the flow finishes, with `code` and `state` or with `error` and `state`. **Its origin must be one of the app\'s `allowed_origins`**; a failure here is refused flat, with no redirect, because until the target is confirmed there is nowhere trusted to bounce a browser to.',
250
+ }),
251
+ state: z.string().min(8).max(512).meta({
252
+ description: 'Returned unchanged on the callback, and on the error redirect too, so the app can bind either answer to the request it started. At least eight characters: this is what ties the callback to the browser that began the flow, and a guessable value defends nothing.',
253
+ }),
254
+ code_challenge: z.string().regex(/^[A-Za-z0-9_-]{43,128}$/).meta({
255
+ description: 'The app\'s own PKCE challenge (RFC 7636, S256 base64url). The verifier is presented at `oidc/exchange`, so the one-time code is worth nothing to whoever intercepts the redirect. `plain` is not accepted: a challenge equal to its verifier defends against nothing.',
256
+ }),
257
+ });
258
+ /**
259
+ * **The query of `GET /api/client/oidc/callback` — the identity provider's
260
+ * wire, not Fleetless's.**
261
+ *
262
+ * Every field but `state` is optional and **the object is not `.strict()`**,
263
+ * which is the whole point of writing it down. A conforming provider sends
264
+ * `code` and `state` on success and `error` (with an optional
265
+ * `error_description`) on refusal, and many send more besides — `iss` per RFC
266
+ * 9207, `session_state`, a vendor field. A strict schema over somebody else's
267
+ * specification refuses conforming callers, which is the mistake
268
+ * `POST /mcp/oauth/register` documents having avoided by not parsing its body
269
+ * at all. Declaring the shape loosely says what arrives without promising it is
270
+ * the only thing that will.
271
+ *
272
+ * `state` is the one required field because it is the one Fleetless minted: it
273
+ * resolves the `oidc_interactions` row that holds the app's `redirect_uri`,
274
+ * and without it there is nowhere to send any answer, success or failure. That
275
+ * is the single case where the cloud renders a page of its own (D2).
276
+ *
277
+ * It exists as a schema rather than as four parameters read by hand because
278
+ * the manifest forbids the second: a documented route whose prose names a
279
+ * `?parameter=` must declare what it reads, and every phrase that used to
280
+ * excuse one was removed by writing the schema rather than by rewording.
281
+ */
282
+ export const clientOidcCallbackQuery = z.object({
283
+ state: z.string().min(1).meta({
284
+ description: 'The opaque state Fleetless sent to the provider, which resolves the pending interaction — and with it the app\'s `redirect_uri`. Not the app\'s own `state` from `start`: that one is stored on the interaction and put back on the redirect to the app. A callback whose state resolves to nothing has no confirmed target to answer, and is the one case Fleetless renders a page for.',
285
+ }),
286
+ code: z.string().min(1).optional().meta({
287
+ description: 'The provider\'s authorization code, present when the sign-in succeeded. Exchanged server-side by the cloud, so it never reaches the app — the app gets its own one-time code, bound to the PKCE challenge it sent at `start`.',
288
+ }),
289
+ error: z.string().min(1).optional().meta({
290
+ description: 'The provider\'s own refusal, present instead of `code` when the person declined or the provider would not issue one. It is carried back to the app as a `clientOidcErrorCode`, not passed through: the provider\'s vocabulary is its own, and an app branching on it would be branching on a string nobody here controls.',
291
+ }),
292
+ error_description: z.string().optional().meta({
293
+ description: 'The provider\'s human-readable note about `error`, when it sends one. Logged, never rendered to an app user and never put on the redirect — it is text from a system Fleetless does not run.',
294
+ }),
295
+ });
296
+ /** Trading the one-time code for a session. The code lives 60 seconds and is bound to the challenge from `start`. */
297
+ export const clientOidcExchangeRequest = z
298
+ .object({
299
+ code: z.string().min(1).meta({
300
+ description: 'The one-time code from the callback redirect. Valid 60 seconds, single-use, and bound to the PKCE challenge the start step carried.',
301
+ }),
302
+ code_verifier: z.string().regex(/^[A-Za-z0-9_.~-]{43,128}$/).meta({
303
+ description: 'The verifier for the challenge sent at `start`. RFC 7636 §4.1\'s alphabet and length.',
304
+ }),
305
+ })
306
+ .strict();
307
+ /**
308
+ * **Why a federated sign-in ended without a session, in a code the app can
309
+ * branch on** — carried back to the app's own `redirect_uri` as `error`, not
310
+ * rendered by Fleetless (D2). The only Fleetless-rendered page in this flow is
311
+ * the one for a state that can no longer be resolved to a redirect URI, because
312
+ * then there is nowhere to send the answer.
313
+ *
314
+ * The five rows of D4's table are the first five values plus `no_access`:
315
+ *
316
+ * - `no_access` — the identity is unknown and nothing admits it, or the account
317
+ * it names is not `active`. **One code for both**, because to the person the
318
+ * remedy is the same — ask somebody to let you in — and a code that split an
319
+ * outcome nobody acts on differently would tell a stranger which half applied.
320
+ * - `email_taken` — the address already belongs to another app user, and the
321
+ * provider is not permitted to link (`link_verified_emails`, or the provider
322
+ * did not assert `email_verified`). Deliberately not `no_access`: the remedy
323
+ * is different — *sign in the way you signed up*.
324
+ * - `email_unverified` — the provider asserted an address without
325
+ * `email_verified`. **An unverified address never produces or links an
326
+ * account**, whatever the rest of the policy says.
327
+ * - `domain_not_allowed`, `registration_closed` — the self-registration policy
328
+ * refused. Honest, because neither is about whether a person exists.
329
+ * - `idp_unavailable`, `exchange_failed`, `claims_incomplete`,
330
+ * `provider_misconfigured`, `provider_disabled` — the provider's or the
331
+ * developer's to fix, and the app can say so.
332
+ * - `invalid_request` — the start parameters did not hold up.
333
+ * - `quota_exceeded` — the org has as many app users as its `max_end_users`
334
+ * quota allows, so no account can be created for this identity. Named rather
335
+ * than folded into `no_access`, for `domain_not_allowed`'s reason: it is not
336
+ * about the person, the app can say what happened, and the remedy belongs to
337
+ * the developer rather than to whoever is trying to sign in. It is raised
338
+ * **only where an account would be created** — an identity that already has
339
+ * one signs in at the quota exactly as it does under it, because refusing a
340
+ * sign-in would turn a protection limit into an outage.
341
+ */
342
+ export const clientOidcErrorCode = z.enum([
343
+ 'no_access',
344
+ 'email_taken',
345
+ 'email_unverified',
346
+ 'domain_not_allowed',
347
+ 'registration_closed',
348
+ 'idp_unavailable',
349
+ 'exchange_failed',
350
+ 'claims_incomplete',
351
+ 'provider_misconfigured',
352
+ 'provider_disabled',
353
+ 'invalid_request',
354
+ 'quota_exceeded',
355
+ ]);
356
+ /* ------------------------------------------------ MCP, delegated login -- */
357
+ /**
358
+ * **A pending MCP authorization, as the app's own consent screen reads it**
359
+ * (D7). Fleetless renders no page here either: `authorize` redirects to the
360
+ * app's `mcp_login_url` with an interaction id, the app authenticates the user
361
+ * with its normal UI, shows this, and approves or denies through the API.
362
+ *
363
+ * `client_name_verified` is `z.literal(false)`, and that is the whole point of
364
+ * the field. The name comes from an **unauthenticated** dynamic registration —
365
+ * the client typed it about itself, nobody checked it — so a consent screen
366
+ * that rendered it as though it were an identity would be teaching people to
367
+ * trust a string an attacker chooses. A literal rather than a boolean because
368
+ * there is no verified case to distinguish: an app that reads this field at all
369
+ * has to handle the untrusted one, and a `true` branch would be dead code
370
+ * pretending to be a safeguard.
371
+ */
372
+ export const clientMcpInteraction = z.object({
373
+ id: z.string().meta({ description: 'The interaction, as it arrived in the app\'s `mcp_login_url`. Not a credential: it names a pending request the server already holds, and approving it needs the app user\'s own access token.' }),
374
+ 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.' }),
375
+ client_name: z.string().nullable().meta({ description: 'What the MCP client calls itself, or `null` if it named nothing. **Unverified** — see `client_name_verified`.' }),
376
+ client_name_verified: z.literal(false).meta({
377
+ 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.',
378
+ }),
379
+ scopes: z.array(z.string()).meta({ description: 'The scopes the client asked for, to show the person before they approve.' }),
380
+ 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.' }),
381
+ expires_at: z.iso.datetime().meta({ description: 'When the interaction stops being approvable. Ten minutes from the authorize step; afterwards both approve and deny answer `interaction_expired`.' }),
382
+ });
383
+ /**
384
+ * What approve and deny both answer: **where to send the browser**. A denial
385
+ * carries a redirect too, with `error=access_denied` on it — a client that is
386
+ * refused must learn so from its own callback rather than from a page nobody
387
+ * sent it.
388
+ */
389
+ export const clientMcpInteractionDecisionResponse = z.object({
390
+ redirect_to: z.url().meta({
391
+ description: 'Send the browser here. It is the MCP client\'s own callback, carrying either the authorization code or `error=access_denied` — a denial redirects as well, so the client learns the outcome from the place it is waiting.',
392
+ }),
393
+ });
394
+ /**
395
+ * **One standing MCP consent, as both withdrawal doors list it.**
396
+ *
397
+ * A grant is what lets a later authorization skip the app's consent screen:
398
+ * `clientMcpInteraction.already_granted` is a read of exactly this row. It is
399
+ * written when a person approves and it is removed by neither the client's
400
+ * registration lapsing nor its access token expiring — so without a door it
401
+ * was a decision a person could make once and never unmake.
402
+ *
403
+ * **Standing only.** A withdrawn grant is stamped rather than deleted, so the
404
+ * store still holds it; neither listing returns one. The question both doors
405
+ * ask is *what is connected right now*, and a row that answered "connected,
406
+ * but no" would be a state every caller has to filter for itself.
407
+ *
408
+ * `client_name_verified` is `z.literal(false)` for the reason
409
+ * `clientMcpInteraction` gives at length: the name comes from an
410
+ * unauthenticated dynamic registration, the client chose it about itself, and
411
+ * a list that rendered it as an identity would be teaching people to trust a
412
+ * string an attacker picked. Here it matters more than on the consent screen,
413
+ * not less — a "connected apps" list is read long after the moment of
414
+ * approval, when nobody remembers what they clicked.
415
+ */
416
+ export const mcpConsentGrant = z.object({
417
+ client_id: z.string().meta({
418
+ 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.',
419
+ }),
420
+ client_name: z.string().nullable().meta({
421
+ 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`.',
422
+ }),
423
+ client_name_verified: z.literal(false).meta({
424
+ 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.',
425
+ }),
426
+ granted_at: z.iso.datetime().meta({
427
+ 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.',
428
+ }),
429
+ });
430
+ /** What both grant listings answer. Never null: a person who has connected nothing gets an empty array, and an absent key would make "nothing" and "not answered" the same reading. */
431
+ export const mcpConsentGrantListResponse = z.object({
432
+ grants: z.array(mcpConsentGrant).meta({
433
+ description: 'Every standing consent this app user holds, newest first. Withdrawn ones are absent rather than listed as withdrawn; an app user who has connected no MCP client answers an empty array.',
434
+ }),
435
+ });
436
+ /* -------------------------------------------------------- who am I -- */
437
+ /**
438
+ * Who the caller turned out to be. Returned by the "who am I" endpoint and by
439
+ * the realtime `auth_ok` frame, so a client can render a session without
440
+ * decoding a token itself — decoding a JWT in the client is how apps end up
441
+ * trusting claims nobody verified.
442
+ *
443
+ * **Three kinds of caller reach the client API.** Besides app users and server
444
+ * keys, a **developer** does: the console's playground runs over the real
445
+ * client API and appears in the audit as the developer, and the console's own
446
+ * live views subscribe on `/realtime` as one. A developer is **org-scoped, not
447
+ * app-scoped** — they own the configuration of every robot in their org — so
448
+ * `app_id` and `role_id` are null for them, and roles do not filter what they
449
+ * see. `kind` states this explicitly rather than leaving it to be inferred from
450
+ * which id happens to be set.
451
+ *
452
+ * **`end_user_id` became `app_user_id`, and that is a rename with a meaning.**
453
+ * The old subject was a member of the org's one pool, reachable through an
454
+ * assignment; the new one is a row that belongs to exactly one app. Renaming
455
+ * rather than keeping the key is deliberate: a consumer reading `.end_user_id`
456
+ * would have typechecked and meant something subtly different, which is the
457
+ * quietest way for a cut like this to go wrong.
458
+ *
459
+ * **`act` is gone.** It named the org admin behind an impersonation (the RFC
460
+ * 8693 pattern). Impersonation is deleted with no successor (D1), so a field
461
+ * that could still arrive would describe a delegation nothing can mint — and a
462
+ * client rendering "you are acting as …" from it would be showing a state the
463
+ * platform cannot enter.
464
+ */
465
+ export const clientIdentity = z.object({
466
+ kind: z.enum(['developer', 'app_user', 'server_key']).meta({
467
+ 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.',
468
+ }),
469
+ developer_id: z.uuid().nullable().meta({
470
+ description: 'The Fleetless user behind this session, or `null` when `kind` is not `developer`.',
471
+ }),
472
+ app_user_id: z.uuid().nullable().meta({
473
+ description: 'The app user behind this session, or `null` when `kind` is not `app_user`. An app user belongs to exactly one app and is unrelated to any Fleetless user with the same address.',
474
+ }),
475
+ server_key_id: z.uuid().nullable().meta({
476
+ description: 'The server key this session was authenticated with, or `null` when `kind` is not `server_key`.',
477
+ }),
478
+ app_id: z.uuid().nullable().meta({
479
+ description: 'The app this session belongs to, and `null` for a developer — a developer is organisation-scoped and owns the configuration of every robot in the organisation rather than reaching one through an app.',
480
+ }),
481
+ role_id: z.uuid().nullable().meta({
482
+ description: 'The role that decides what this caller may reach, and `null` for a developer. Roles are the only visibility filter: what a role does not grant does not exist for that user.',
483
+ }),
484
+ email: z.email().nullable().meta({
485
+ description: 'The address of the Fleetless user or app user behind this session, and `null` for a server key, which is not a person.',
486
+ }),
487
+ });