@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.
- package/CHANGELOG.md +26 -0
- package/LICENSE +202 -0
- package/NOTICE +17 -0
- package/README.md +88 -0
- package/artifacts/constants.json +24 -0
- package/artifacts/openapi.json +17219 -0
- package/artifacts/routes.json +4605 -0
- package/artifacts/schema/accept-team-invite-request.schema.json +22 -0
- package/artifacts/schema/action-config.schema.json +198 -0
- package/artifacts/schema/alert-list-response.schema.json +172 -0
- package/artifacts/schema/api-error.schema.json +20 -0
- package/artifacts/schema/app-auth-config.schema.json +106 -0
- package/artifacts/schema/app-invitation-list-response.schema.json +57 -0
- package/artifacts/schema/app-invitation.schema.json +69 -0
- package/artifacts/schema/app-list-response.schema.json +82 -0
- package/artifacts/schema/app-mail-template-list-response.schema.json +68 -0
- package/artifacts/schema/app-mail-template.schema.json +54 -0
- package/artifacts/schema/app-oidc-provider-list-response.schema.json +93 -0
- package/artifacts/schema/app-oidc-provider.schema.json +80 -0
- package/artifacts/schema/app-user-list-response.schema.json +111 -0
- package/artifacts/schema/app-user.schema.json +98 -0
- package/artifacts/schema/app.schema.json +69 -0
- package/artifacts/schema/apply-error.schema.json +41 -0
- package/artifacts/schema/asset-list-response.schema.json +288 -0
- package/artifacts/schema/asset-sync-request.schema.json +17 -0
- package/artifacts/schema/asset-sync-response.schema.json +16 -0
- package/artifacts/schema/asset-sync-status.schema.json +136 -0
- package/artifacts/schema/asset.schema.json +68 -0
- package/artifacts/schema/audit-actor.schema.json +32 -0
- package/artifacts/schema/audit-event.schema.json +119 -0
- package/artifacts/schema/audit-list-response.schema.json +144 -0
- package/artifacts/schema/audit-query.schema.json +79 -0
- package/artifacts/schema/auth-error.schema.json +23 -0
- package/artifacts/schema/auth-me-response.schema.json +99 -0
- package/artifacts/schema/auth-ok.schema.json +115 -0
- package/artifacts/schema/authorization-server-metadata.schema.json +80 -0
- package/artifacts/schema/bridge-asset-progress.schema.json +99 -0
- package/artifacts/schema/bridge-assets-available.schema.json +25 -0
- package/artifacts/schema/bridge-camera-state.schema.json +78 -0
- package/artifacts/schema/bridge-config-applied.schema.json +67 -0
- package/artifacts/schema/bridge-hello.schema.json +65 -0
- package/artifacts/schema/bridge-introspect.schema.json +114 -0
- package/artifacts/schema/bridge-job-lost.schema.json +22 -0
- package/artifacts/schema/bridge-job-update.schema.json +100 -0
- package/artifacts/schema/bridge-pong.schema.json +19 -0
- package/artifacts/schema/bridge-pressure.schema.json +292 -0
- package/artifacts/schema/bridge-state.schema.json +24 -0
- package/artifacts/schema/bridge-type-definitions.schema.json +169 -0
- package/artifacts/schema/busy-details.schema.json +115 -0
- package/artifacts/schema/camera-descriptor.schema.json +45 -0
- package/artifacts/schema/camera-list-response.schema.json +58 -0
- package/artifacts/schema/camera-source.schema.json +240 -0
- package/artifacts/schema/cancel-request.schema.json +20 -0
- package/artifacts/schema/client-accept-invitation-request.schema.json +35 -0
- package/artifacts/schema/client-auth.schema.json +18 -0
- package/artifacts/schema/client-cancel.schema.json +45 -0
- package/artifacts/schema/client-identity.schema.json +103 -0
- package/artifacts/schema/client-invoke.schema.json +45 -0
- package/artifacts/schema/client-login-request.schema.json +29 -0
- package/artifacts/schema/client-logout-request.schema.json +14 -0
- package/artifacts/schema/client-mcp-interaction-decision-response.schema.json +15 -0
- package/artifacts/schema/client-mcp-interaction.schema.json +59 -0
- package/artifacts/schema/client-oidc-callback-query.schema.json +28 -0
- package/artifacts/schema/client-oidc-exchange-request.schema.json +21 -0
- package/artifacts/schema/client-oidc-start-query.schema.json +36 -0
- package/artifacts/schema/client-password-reset-confirm-request.schema.json +22 -0
- package/artifacts/schema/client-password-reset-request.schema.json +24 -0
- package/artifacts/schema/client-provider-list-query.schema.json +17 -0
- package/artifacts/schema/client-provider-list-response.schema.json +34 -0
- package/artifacts/schema/client-publish.schema.json +40 -0
- package/artifacts/schema/client-refresh-request.schema.json +14 -0
- package/artifacts/schema/client-register-request.schema.json +44 -0
- package/artifacts/schema/client-resend-verification-request.schema.json +24 -0
- package/artifacts/schema/client-subscribe.schema.json +43 -0
- package/artifacts/schema/client-unsubscribe.schema.json +26 -0
- package/artifacts/schema/client-verify-email-request.schema.json +15 -0
- package/artifacts/schema/cloud-asset-request.schema.json +37 -0
- package/artifacts/schema/cloud-camera-start.schema.json +41 -0
- package/artifacts/schema/cloud-camera-stop.schema.json +26 -0
- package/artifacts/schema/cloud-cancel.schema.json +33 -0
- package/artifacts/schema/cloud-config.schema.json +1635 -0
- package/artifacts/schema/cloud-hello-error.schema.json +23 -0
- package/artifacts/schema/cloud-hello-ok.schema.json +19 -0
- package/artifacts/schema/cloud-introspect-request.schema.json +19 -0
- package/artifacts/schema/cloud-invoke.schema.json +40 -0
- package/artifacts/schema/cloud-ping.schema.json +19 -0
- package/artifacts/schema/cloud-publish.schema.json +28 -0
- package/artifacts/schema/cloud-type-request.schema.json +30 -0
- package/artifacts/schema/command-result.schema.json +175 -0
- package/artifacts/schema/config-draft-response.schema.json +1695 -0
- package/artifacts/schema/config-state.schema.json +124 -0
- package/artifacts/schema/config-version-response.schema.json +1641 -0
- package/artifacts/schema/config-versions-response.schema.json +33 -0
- package/artifacts/schema/create-app-invitation-request.schema.json +40 -0
- package/artifacts/schema/create-app-oidc-provider-request.schema.json +70 -0
- package/artifacts/schema/create-app-request.schema.json +30 -0
- package/artifacts/schema/create-app-user-request.schema.json +42 -0
- package/artifacts/schema/create-robot-request.schema.json +14 -0
- package/artifacts/schema/create-robot-response.schema.json +44 -0
- package/artifacts/schema/create-server-key-response.schema.json +65 -0
- package/artifacts/schema/create-team-invite-request.schema.json +43 -0
- package/artifacts/schema/datapoint-alert-row.schema.json +160 -0
- package/artifacts/schema/datapoint-config.schema.json +366 -0
- package/artifacts/schema/datapoint-display.schema.json +31 -0
- package/artifacts/schema/datapoint-event.schema.json +34 -0
- package/artifacts/schema/datapoint-frame.schema.json +28 -0
- package/artifacts/schema/datapoint-list-response.schema.json +61 -0
- package/artifacts/schema/datapoint-value.schema.json +28 -0
- package/artifacts/schema/developer-login-request.schema.json +19 -0
- package/artifacts/schema/dynamic-client-registration-request.schema.json +60 -0
- package/artifacts/schema/dynamic-client-registration-response.schema.json +68 -0
- package/artifacts/schema/error-frame.schema.json +23 -0
- package/artifacts/schema/exposure-counts.schema.json +39 -0
- package/artifacts/schema/exposure-list-response.schema.json +43 -0
- package/artifacts/schema/fetch-types-request.schema.json +19 -0
- package/artifacts/schema/fetch-types-response.schema.json +163 -0
- package/artifacts/schema/fleetless-user-list-response.schema.json +73 -0
- package/artifacts/schema/fleetless-user.schema.json +60 -0
- package/artifacts/schema/history-buckets-response.schema.json +79 -0
- package/artifacts/schema/history-query.schema.json +58 -0
- package/artifacts/schema/history-response.schema.json +150 -0
- package/artifacts/schema/history-samples-response.schema.json +68 -0
- package/artifacts/schema/introspection-response.schema.json +118 -0
- package/artifacts/schema/invoke-or-service-response.schema.json +141 -0
- package/artifacts/schema/invoke-request.schema.json +23 -0
- package/artifacts/schema/invoke-response.schema.json +125 -0
- package/artifacts/schema/job-actor.schema.json +34 -0
- package/artifacts/schema/job-event.schema.json +158 -0
- package/artifacts/schema/job-response.schema.json +123 -0
- package/artifacts/schema/job-run-list-response.schema.json +222 -0
- package/artifacts/schema/job-run-query.schema.json +95 -0
- package/artifacts/schema/job-run-summary-query.schema.json +23 -0
- package/artifacts/schema/job-run-summary.schema.json +33 -0
- package/artifacts/schema/job-run.schema.json +195 -0
- package/artifacts/schema/job-state.schema.json +11 -0
- package/artifacts/schema/job.schema.json +106 -0
- package/artifacts/schema/latency-bucket.schema.json +63 -0
- package/artifacts/schema/live-session-response.schema.json +41 -0
- package/artifacts/schema/mail-outcome.schema.json +20 -0
- package/artifacts/schema/mail-template-preview-request.schema.json +35 -0
- package/artifacts/schema/mail-template-preview-response.schema.json +31 -0
- package/artifacts/schema/mail-template-problem-details.schema.json +24 -0
- package/artifacts/schema/mcp-consent-grant-list-response.schema.json +52 -0
- package/artifacts/schema/mcp-consent-grant.schema.json +39 -0
- package/artifacts/schema/mcp-robot-datasheet.schema.json +115 -0
- package/artifacts/schema/mcp-role-preview-response.schema.json +134 -0
- package/artifacts/schema/missing-asset-query.schema.json +11 -0
- package/artifacts/schema/oauth-authorize-query.schema.json +47 -0
- package/artifacts/schema/oauth-redirect-response.schema.json +15 -0
- package/artifacts/schema/oauth-token-request.schema.json +47 -0
- package/artifacts/schema/oauth-token-response.schema.json +38 -0
- package/artifacts/schema/org-alerts-query.schema.json +15 -0
- package/artifacts/schema/org-event-dropped.schema.json +26 -0
- package/artifacts/schema/org-event-replay.schema.json +97 -0
- package/artifacts/schema/org-event-subscribe.schema.json +14 -0
- package/artifacts/schema/org-event-unsubscribe.schema.json +14 -0
- package/artifacts/schema/org-event.schema.json +75 -0
- package/artifacts/schema/org-firing-alerts-response.schema.json +178 -0
- package/artifacts/schema/org-health-query.schema.json +13 -0
- package/artifacts/schema/org-latency-query.schema.json +42 -0
- package/artifacts/schema/org-latency-response.schema.json +124 -0
- package/artifacts/schema/org-quota-usage-counts.schema.json +42 -0
- package/artifacts/schema/org-quota-usage.schema.json +102 -0
- package/artifacts/schema/org-quotas.schema.json +51 -0
- package/artifacts/schema/org-usage-query.schema.json +19 -0
- package/artifacts/schema/org-usage-response.schema.json +77 -0
- package/artifacts/schema/org.schema.json +30 -0
- package/artifacts/schema/parameter-invalid-details.schema.json +37 -0
- package/artifacts/schema/parameter-spec.schema.json +120 -0
- package/artifacts/schema/parameter-violation.schema.json +24 -0
- package/artifacts/schema/password-change-request.schema.json +21 -0
- package/artifacts/schema/password-reset-confirm.schema.json +19 -0
- package/artifacts/schema/password-reset-request.schema.json +14 -0
- package/artifacts/schema/patch-app-oidc-provider-request.schema.json +50 -0
- package/artifacts/schema/patch-app-user-request.schema.json +34 -0
- package/artifacts/schema/patch-auth-me-request.schema.json +22 -0
- package/artifacts/schema/patch-fleetless-user-request.schema.json +20 -0
- package/artifacts/schema/patch-org-request.schema.json +15 -0
- package/artifacts/schema/patch-org-response.schema.json +40 -0
- package/artifacts/schema/patch-robot-request.schema.json +15 -0
- package/artifacts/schema/patch-robot-response.schema.json +40 -0
- package/artifacts/schema/pending-team-invite-list-response.schema.json +52 -0
- package/artifacts/schema/pending-team-invite.schema.json +39 -0
- package/artifacts/schema/protected-resource-metadata.schema.json +41 -0
- package/artifacts/schema/publish-config-response.schema.json +21 -0
- package/artifacts/schema/publish-request.schema.json +17 -0
- package/artifacts/schema/publisher-config.schema.json +285 -0
- package/artifacts/schema/put-app-auth-config-request.schema.json +93 -0
- package/artifacts/schema/put-app-mail-template-request.schema.json +35 -0
- package/artifacts/schema/put-config-draft-request.schema.json +13 -0
- package/artifacts/schema/put-datapoint-display-request.schema.json +31 -0
- package/artifacts/schema/put-robot-details-request.schema.json +41 -0
- package/artifacts/schema/put-robot-details-response.schema.json +43 -0
- package/artifacts/schema/rate-limit-details.schema.json +15 -0
- package/artifacts/schema/refresh-request.schema.json +13 -0
- package/artifacts/schema/release-live-query.schema.json +13 -0
- package/artifacts/schema/rename-slug-request.schema.json +23 -0
- package/artifacts/schema/rename-slug-response.schema.json +24 -0
- package/artifacts/schema/resource-health-event.schema.json +72 -0
- package/artifacts/schema/resource-health-list-response.schema.json +80 -0
- package/artifacts/schema/resource-health-state.schema.json +68 -0
- package/artifacts/schema/robot-config-doc.schema.json +1616 -0
- package/artifacts/schema/robot-delete-query.schema.json +12 -0
- package/artifacts/schema/robot-deletion-summary.schema.json +63 -0
- package/artifacts/schema/robot-detail-response.schema.json +262 -0
- package/artifacts/schema/robot-details-doc.schema.json +33 -0
- package/artifacts/schema/robot-jobs-response.schema.json +119 -0
- package/artifacts/schema/robot-latency-series.schema.json +81 -0
- package/artifacts/schema/robot-list-item.schema.json +94 -0
- package/artifacts/schema/robot-list-response.schema.json +106 -0
- package/artifacts/schema/robot.schema.json +30 -0
- package/artifacts/schema/role-list-response.schema.json +48 -0
- package/artifacts/schema/role-permissions.schema.json +61 -0
- package/artifacts/schema/role.schema.json +35 -0
- package/artifacts/schema/ros-graph.schema.json +99 -0
- package/artifacts/schema/server-key-list-response.schema.json +64 -0
- package/artifacts/schema/server-key.schema.json +51 -0
- package/artifacts/schema/service-call-response.schema.json +13 -0
- package/artifacts/schema/service-config.schema.json +198 -0
- package/artifacts/schema/session-tokens.schema.json +28 -0
- package/artifacts/schema/sign-up-request.schema.json +26 -0
- package/artifacts/schema/sign-up-response.schema.json +127 -0
- package/artifacts/schema/slug-usage-response.schema.json +32 -0
- package/artifacts/schema/snapshot-header.schema.json +44 -0
- package/artifacts/schema/snapshot-meta-response.schema.json +85 -0
- package/artifacts/schema/subscribe-error.schema.json +31 -0
- package/artifacts/schema/team-invite.schema.json +57 -0
- package/artifacts/schema/tier-change-request.schema.json +17 -0
- package/artifacts/schema/type-definition.schema.json +144 -0
- package/artifacts/schema/types-response.schema.json +156 -0
- package/artifacts/schema/update-app-request.schema.json +32 -0
- package/artifacts/schema/urdf-completeness.schema.json +50 -0
- package/artifacts/schema/validation-issue.schema.json +43 -0
- package/artifacts/schema/waitlist-request.schema.json +15 -0
- package/artifacts/schema-outgoing/bridge-asset-progress.schema.json +102 -0
- package/artifacts/schema-outgoing/bridge-assets-available.schema.json +26 -0
- package/artifacts/schema-outgoing/bridge-camera-state.schema.json +80 -0
- package/artifacts/schema-outgoing/bridge-config-applied.schema.json +69 -0
- package/artifacts/schema-outgoing/bridge-hello.schema.json +68 -0
- package/artifacts/schema-outgoing/bridge-introspect.schema.json +119 -0
- package/artifacts/schema-outgoing/bridge-job-lost.schema.json +23 -0
- package/artifacts/schema-outgoing/bridge-job-update.schema.json +102 -0
- package/artifacts/schema-outgoing/bridge-pong.schema.json +20 -0
- package/artifacts/schema-outgoing/bridge-type-definitions.schema.json +174 -0
- package/artifacts/schema-outgoing/datapoint-frame.schema.json +29 -0
- package/artifacts/schema-outgoing/snapshot-header.schema.json +45 -0
- package/dist/alerts.d.ts +255 -0
- package/dist/alerts.js +193 -0
- package/dist/app-users.d.ts +606 -0
- package/dist/app-users.js +696 -0
- package/dist/apps.d.ts +175 -0
- package/dist/apps.js +267 -0
- package/dist/assets.d.ts +434 -0
- package/dist/assets.js +546 -0
- package/dist/audit.d.ts +129 -0
- package/dist/audit.js +238 -0
- package/dist/client-auth.d.ts +409 -0
- package/dist/client-auth.js +487 -0
- package/dist/common.d.ts +186 -0
- package/dist/common.js +199 -0
- package/dist/config-issues.d.ts +175 -0
- package/dist/config-issues.js +339 -0
- package/dist/config.d.ts +862 -0
- package/dist/config.js +1988 -0
- package/dist/errors.d.ts +52 -0
- package/dist/errors.js +786 -0
- package/dist/identity.d.ts +549 -0
- package/dist/identity.js +503 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +51 -0
- package/dist/introspection.d.ts +99 -0
- package/dist/introspection.js +97 -0
- package/dist/jobs.d.ts +334 -0
- package/dist/jobs.js +345 -0
- package/dist/mcp.d.ts +239 -0
- package/dist/mcp.js +153 -0
- package/dist/oauth.d.ts +344 -0
- package/dist/oauth.js +488 -0
- package/dist/protocol.d.ts +781 -0
- package/dist/protocol.js +715 -0
- package/dist/realtime.d.ts +494 -0
- package/dist/realtime.js +512 -0
- package/dist/rest.d.ts +1989 -0
- package/dist/rest.js +1963 -0
- package/dist/routes.d.ts +94 -0
- package/dist/routes.js +2298 -0
- 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
|
+
});
|