@fleetless/contracts 5.3.0-next.1 → 6.0.0-next.1

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 (89) hide show
  1. package/CHANGELOG.md +96 -33
  2. package/artifacts/openapi.json +2049 -773
  3. package/artifacts/routes.json +1665 -314
  4. package/artifacts/schema/accept-team-invite-request.schema.json +13 -7
  5. package/artifacts/schema/app-auth-config.schema.json +108 -4
  6. package/artifacts/schema/app-hosted-pages.schema.json +33 -0
  7. package/artifacts/schema/app-invitation.schema.json +2 -2
  8. package/artifacts/schema/app-mail-template-list-response.schema.json +4 -3
  9. package/artifacts/schema/app-mail-template.schema.json +3 -2
  10. package/artifacts/schema/app-sign-in-methods.schema.json +19 -0
  11. package/artifacts/schema/app-user-list-response.schema.json +36 -0
  12. package/artifacts/schema/app-user.schema.json +36 -0
  13. package/artifacts/schema/audit-query.schema.json +0 -6
  14. package/artifacts/schema/auth-me-response.schema.json +27 -0
  15. package/artifacts/schema/auth-ok.schema.json +13 -1
  16. package/artifacts/schema/client-accept-invitation-request.schema.json +3 -4
  17. package/artifacts/schema/client-identity.schema.json +13 -1
  18. package/artifacts/schema/client-login-code-request.schema.json +24 -0
  19. package/artifacts/schema/client-login-code-verify-request.schema.json +30 -0
  20. package/artifacts/schema/client-provider-list-response.schema.json +22 -2
  21. package/artifacts/schema/client-register-request.schema.json +3 -4
  22. package/artifacts/schema/client-sign-in-result.schema.json +55 -0
  23. package/artifacts/schema/client-two-factor-disable-request.schema.json +15 -0
  24. package/artifacts/schema/client-two-factor-setup-confirm-request.schema.json +20 -0
  25. package/artifacts/schema/client-two-factor-setup-confirm-response.schema.json +49 -0
  26. package/artifacts/schema/client-two-factor-setup-request.schema.json +12 -0
  27. package/artifacts/schema/client-two-factor-verify-request.schema.json +25 -0
  28. package/artifacts/schema/create-app-invitation-request.schema.json +1 -1
  29. package/artifacts/schema/create-passkey-request.schema.json +25 -0
  30. package/artifacts/schema/create-passkey-response.schema.json +84 -0
  31. package/artifacts/schema/developer-passkey.schema.json +56 -0
  32. package/artifacts/schema/developer-two-factor.schema.json +105 -0
  33. package/artifacts/schema/fleetless-user-list-response.schema.json +22 -0
  34. package/artifacts/schema/fleetless-user.schema.json +22 -0
  35. package/artifacts/schema/invalid-code-details.schema.json +16 -0
  36. package/artifacts/schema/job-actor.schema.json +1 -15
  37. package/artifacts/schema/job-run-list-response.schema.json +1 -15
  38. package/artifacts/schema/job-run.schema.json +1 -15
  39. package/artifacts/schema/org.schema.json +5 -0
  40. package/artifacts/schema/patch-org-request.schema.json +5 -3
  41. package/artifacts/schema/patch-org-response.schema.json +5 -0
  42. package/artifacts/schema/put-app-auth-look-request.schema.json +22 -0
  43. package/artifacts/schema/put-app-auth-mcp-request.schema.json +1 -14
  44. package/artifacts/schema/put-app-auth-sign-in-request.schema.json +39 -0
  45. package/artifacts/schema/put-app-auth-urls-request.schema.json +31 -4
  46. package/artifacts/schema/recovery-codes-response.schema.json +20 -0
  47. package/artifacts/schema/{role-rename-request.schema.json → rename-passkey-request.schema.json} +2 -2
  48. package/artifacts/schema/role-list-response.schema.json +1 -1
  49. package/artifacts/schema/role.schema.json +1 -1
  50. package/artifacts/schema/totp-confirm-request.schema.json +15 -0
  51. package/artifacts/schema/totp-confirm-response.schema.json +27 -0
  52. package/artifacts/schema/two-factor-challenge.schema.json +24 -0
  53. package/artifacts/schema/two-factor-setup-response.schema.json +21 -0
  54. package/artifacts/schema/webauthn-options-response.schema.json +18 -0
  55. package/dist/app-users.d.ts +154 -34
  56. package/dist/app-users.js +177 -45
  57. package/dist/apps.d.ts +2 -38
  58. package/dist/apps.js +3 -46
  59. package/dist/audit.d.ts +0 -1
  60. package/dist/audit.js +0 -13
  61. package/dist/client-auth.d.ts +133 -24
  62. package/dist/client-auth.js +139 -28
  63. package/dist/config.d.ts +2 -2
  64. package/dist/errors.d.ts +10 -1
  65. package/dist/errors.js +34 -34
  66. package/dist/identity.d.ts +172 -123
  67. package/dist/identity.js +189 -101
  68. package/dist/index.d.ts +11 -13
  69. package/dist/index.js +12 -10
  70. package/dist/jobs.d.ts +0 -3
  71. package/dist/jobs.js +0 -16
  72. package/dist/protocol.d.ts +1 -1
  73. package/dist/realtime.d.ts +1 -0
  74. package/dist/rest.d.ts +2 -2
  75. package/dist/rest.js +17 -28
  76. package/dist/routes.d.ts +19 -0
  77. package/dist/routes.js +499 -223
  78. package/package.json +1 -1
  79. package/artifacts/schema/developer-login-request.schema.json +0 -19
  80. package/artifacts/schema/feedback-request.schema.json +0 -34
  81. package/artifacts/schema/feedback-response.schema.json +0 -27
  82. package/artifacts/schema/password-reset-confirm.schema.json +0 -19
  83. package/artifacts/schema/password-reset-request.schema.json +0 -14
  84. package/artifacts/schema/role-delete-query.schema.json +0 -13
  85. package/artifacts/schema/role-in-use-details.schema.json +0 -28
  86. package/artifacts/schema/sign-up-request.schema.json +0 -26
  87. package/artifacts/schema/sign-up-response.schema.json +0 -127
  88. package/dist/feedback.d.ts +0 -42
  89. package/dist/feedback.js +0 -35
@@ -3,11 +3,12 @@ import { z } from 'zod';
3
3
  /**
4
4
  * **The client auth API: the whole of what an app user's browser talks to.**
5
5
  *
6
- * Fleetless shows an app user **no page**. The developer's own UI owns
7
- * every screen — login, registration, verification, invitation acceptance,
6
+ * The developer's own UI owns every screen it wants to — login, emailed
7
+ * code, second factor, registration, verification, invitation acceptance,
8
8
  * password reset, the provider buttons, the MCP consent — and calls these
9
- * routes as JSON. The hosted, app-branded login and consent pages this file
10
- * used to describe are deleted.
9
+ * routes as JSON. **Where the app sets no URL, a Fleetless-hosted page on the
10
+ * auth portal stands in** (`appAuthConfig.hosted_pages`) and calls the same
11
+ * policy: one set of rules, whichever page drives it.
11
12
  *
12
13
  * Everything here is public: `app_identifier` travels in the body (in the
13
14
  * query for a GET), CORS is answered only for the app's `allowed_origins`, and
@@ -24,12 +25,19 @@ import { z } from 'zod';
24
25
  * the SDK and the cloud cannot drift into disagreeing about it.
25
26
  *
26
27
  * **The enumeration discipline is deliberate, not a preference:**
27
- * `register`, `resend-verification` and `password/reset` answer `202` for every
28
- * policy-allowed request whether or not the address exists, and `login` answers
29
- * the identical `invalid_credentials` for a wrong password, a `blocked` account
30
- * and a `pending_verification` one. Policy refusals are honest —
31
- * `registration_closed` and `domain_not_allowed` say what they are, because
32
- * neither reveals whether a *person* exists.
28
+ * `register`, `resend-verification`, `password/reset` and `login/code` answer
29
+ * `202` for every policy-allowed request whether or not the address exists,
30
+ * and `login` answers the identical `invalid_credentials` for a wrong
31
+ * password, a `blocked` account and a `pending_verification` one. Policy
32
+ * refusals are honest — `registration_closed`, `domain_not_allowed` and
33
+ * `method_not_allowed` say what they are, because none reveals whether a
34
+ * *person* exists.
35
+ *
36
+ * **Every step that signs somebody in answers a `clientSignInResult`**:
37
+ * session tokens, or a two-factor challenge when the person has a confirmed
38
+ * authenticator or the app requires one. No path yields a session without the
39
+ * second factor then — except a sign-in through an identity provider, which
40
+ * owns that sign-in and answers tokens as before.
33
41
  */
34
42
  export declare const clientLoginRequest: z.ZodObject<{
35
43
  app_identifier: z.ZodString;
@@ -59,6 +67,103 @@ export declare const clientLogoutRequest: z.ZodObject<{
59
67
  refresh_token: z.ZodString;
60
68
  }, z.core.$strip>;
61
69
  export type ClientLogoutRequest = z.infer<typeof clientLogoutRequest>;
70
+ /**
71
+ * **Asking for a sign-in code.** A six-digit code, mailed, valid ten minutes;
72
+ * a new request expires the previous code for the same address. The answer is
73
+ * `202` whether or not the address names an account — a decoy, like
74
+ * `resend-verification` — so this is no enumeration oracle.
75
+ */
76
+ export declare const clientLoginCodeRequest: z.ZodObject<{
77
+ app_identifier: z.ZodString;
78
+ email: z.ZodEmail;
79
+ }, z.core.$strict>;
80
+ export type ClientLoginCodeRequest = z.infer<typeof clientLoginCodeRequest>;
81
+ /** **Spending the code.** Five wrong attempts spend it; so does its tenth minute. */
82
+ export declare const clientLoginCodeVerifyRequest: z.ZodObject<{
83
+ app_identifier: z.ZodString;
84
+ email: z.ZodEmail;
85
+ code: z.ZodString;
86
+ }, z.core.$strict>;
87
+ export type ClientLoginCodeVerifyRequest = z.infer<typeof clientLoginCodeVerifyRequest>;
88
+ /**
89
+ * **A sign-in that needs its second factor first.** No session exists yet:
90
+ * the `challenge` is a short-lived handle, five minutes, that the next call
91
+ * spends.
92
+ *
93
+ * - `two_factor_required` — the person has an authenticator; send a code or
94
+ * a recovery code to `POST /api/client/two-factor/verify`.
95
+ * - `two_factor_setup_required` — the app requires two-factor and the person
96
+ * has none; set one up with `POST /api/client/two-factor/setup` and
97
+ * `…/setup/confirm`, which answers the session.
98
+ */
99
+ export declare const twoFactorChallenge: z.ZodObject<{
100
+ status: z.ZodEnum<{
101
+ two_factor_required: "two_factor_required";
102
+ two_factor_setup_required: "two_factor_setup_required";
103
+ }>;
104
+ challenge: z.ZodString;
105
+ }, z.core.$strip>;
106
+ export type TwoFactorChallenge = z.infer<typeof twoFactorChallenge>;
107
+ /**
108
+ * **What every session-minting sign-in step answers**: password login, code
109
+ * verify, and spending an invitation, verification or reset token. Session
110
+ * tokens when the sign-in is complete; a `twoFactorChallenge` when a second
111
+ * factor comes first. Tell them apart by `status`, which only the challenge
112
+ * carries.
113
+ */
114
+ export declare const clientSignInResult: z.ZodUnion<readonly [z.ZodObject<{
115
+ access_token: z.ZodString;
116
+ refresh_token: z.ZodString;
117
+ expires_in: z.ZodNumber;
118
+ }, z.core.$strip>, z.ZodObject<{
119
+ status: z.ZodEnum<{
120
+ two_factor_required: "two_factor_required";
121
+ two_factor_setup_required: "two_factor_setup_required";
122
+ }>;
123
+ challenge: z.ZodString;
124
+ }, z.core.$strip>]>;
125
+ export type ClientSignInResult = z.infer<typeof clientSignInResult>;
126
+ /** **Answering a `two_factor_required` challenge**, with the authenticator's code or one recovery code — exactly one of the two. */
127
+ export declare const clientTwoFactorVerifyRequest: z.ZodObject<{
128
+ challenge: z.ZodString;
129
+ code: z.ZodOptional<z.ZodString>;
130
+ recovery_code: z.ZodOptional<z.ZodString>;
131
+ }, z.core.$strict>;
132
+ export type ClientTwoFactorVerifyRequest = z.infer<typeof clientTwoFactorVerifyRequest>;
133
+ /**
134
+ * **Starting an authenticator setup.** Either during sign-in, with the
135
+ * `two_factor_setup_required` challenge in the body, or from the app's own
136
+ * account settings, with the app user's bearer and no challenge.
137
+ */
138
+ export declare const clientTwoFactorSetupRequest: z.ZodObject<{
139
+ challenge: z.ZodOptional<z.ZodString>;
140
+ }, z.core.$strict>;
141
+ export type ClientTwoFactorSetupRequest = z.infer<typeof clientTwoFactorSetupRequest>;
142
+ /** **Confirming the setup** with a code from the new authenticator. Only then is it on. */
143
+ export declare const clientTwoFactorSetupConfirmRequest: z.ZodObject<{
144
+ challenge: z.ZodOptional<z.ZodString>;
145
+ code: z.ZodString;
146
+ }, z.core.$strict>;
147
+ export type ClientTwoFactorSetupConfirmRequest = z.infer<typeof clientTwoFactorSetupConfirmRequest>;
148
+ /**
149
+ * **The confirmed setup**: the ten recovery codes, shown this once, and a
150
+ * session — during sign-in the first one, from account settings a fresh one,
151
+ * since every other session of the account ends.
152
+ */
153
+ export declare const clientTwoFactorSetupConfirmResponse: z.ZodObject<{
154
+ recovery_codes: z.ZodArray<z.ZodString>;
155
+ session: z.ZodObject<{
156
+ access_token: z.ZodString;
157
+ refresh_token: z.ZodString;
158
+ expires_in: z.ZodNumber;
159
+ }, z.core.$strip>;
160
+ }, z.core.$strip>;
161
+ export type ClientTwoFactorSetupConfirmResponse = z.infer<typeof clientTwoFactorSetupConfirmResponse>;
162
+ /** **Turning the authenticator off**, from the app's own account settings. A current code proves the person still holds it. */
163
+ export declare const clientTwoFactorDisableRequest: z.ZodObject<{
164
+ code: z.ZodString;
165
+ }, z.core.$strict>;
166
+ export type ClientTwoFactorDisableRequest = z.infer<typeof clientTwoFactorDisableRequest>;
62
167
  /**
63
168
  * **Self-registration** — and the account it creates cannot log in yet.
64
169
  *
@@ -81,7 +186,7 @@ export type ClientLogoutRequest = z.infer<typeof clientLogoutRequest>;
81
186
  export declare const clientRegisterRequest: z.ZodObject<{
82
187
  app_identifier: z.ZodString;
83
188
  email: z.ZodEmail;
84
- password: z.ZodString;
189
+ password: z.ZodOptional<z.ZodString>;
85
190
  display_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
86
191
  }, z.core.$strict>;
87
192
  export type ClientRegisterRequest = z.infer<typeof clientRegisterRequest>;
@@ -97,12 +202,10 @@ export declare const clientResendVerificationRequest: z.ZodObject<{
97
202
  }, z.core.$strict>;
98
203
  export type ClientResendVerificationRequest = z.infer<typeof clientResendVerificationRequest>;
99
204
  /**
100
- * Asking for a reset link **as an app user**.
101
- *
102
- * Same act as `passwordResetRequest`, different shape, because the two surfaces
103
- * identify a person differently. A Fleetless user's address is globally unique
104
- * and resolves alone; an app user's is unique only within their app, so the
105
- * pair is what names them.
205
+ * Asking for a reset link **as an app user** — the only password reset
206
+ * there is: Fleetless users hold no password. An app user's address is
207
+ * unique only within their app, so the pair of app and address is what names
208
+ * them.
106
209
  *
107
210
  * The response is identical for a known and an unknown pair — otherwise this
108
211
  * becomes the enumeration oracle the rest of the family is carefully built not
@@ -132,7 +235,7 @@ export type ClientPasswordResetConfirmRequest = z.infer<typeof clientPasswordRes
132
235
  */
133
236
  export declare const clientAcceptInvitationRequest: z.ZodObject<{
134
237
  token: z.ZodString;
135
- password: z.ZodString;
238
+ password: z.ZodOptional<z.ZodString>;
136
239
  display_name: z.ZodOptional<z.ZodNullable<z.ZodString>>;
137
240
  }, z.core.$strict>;
138
241
  export type ClientAcceptInvitationRequest = z.infer<typeof clientAcceptInvitationRequest>;
@@ -168,8 +271,8 @@ export declare const clientProviderListQuery: z.ZodObject<{
168
271
  }, z.core.$strip>;
169
272
  export type ClientProviderListQuery = z.infer<typeof clientProviderListQuery>;
170
273
  /**
171
- * What the developer's login page needs to draw its provider buttons, and
172
- * **nothing more**. This route is public and unauthenticated: the issuer, the
274
+ * What the developer's login page needs to draw its provider buttons and its
275
+ * password or code fields, and **nothing more**. This route is public and unauthenticated: the issuer, the
173
276
  * client id, the scopes and the linking policy are all management-side facts
174
277
  * that would tell a stranger how the app's federation is configured.
175
278
  *
@@ -181,6 +284,10 @@ export declare const clientProviderListResponse: z.ZodObject<{
181
284
  slug: z.ZodString;
182
285
  name: z.ZodString;
183
286
  }, z.core.$strip>>;
287
+ sign_in_methods: z.ZodObject<{
288
+ password: z.ZodBoolean;
289
+ email_code: z.ZodBoolean;
290
+ }, z.core.$strict>;
184
291
  }, z.core.$strip>;
185
292
  export type ClientProviderListResponse = z.infer<typeof clientProviderListResponse>;
186
293
  /**
@@ -291,10 +398,11 @@ export declare const clientOidcErrorCode: z.ZodEnum<{
291
398
  }>;
292
399
  export type ClientOidcErrorCode = z.infer<typeof clientOidcErrorCode>;
293
400
  /**
294
- * **A pending MCP authorization, as the app's own consent screen reads it**
295
- * Fleetless renders no page here either: `authorize` redirects to the
296
- * app's `mcp_login_url` with an interaction id, the app authenticates the user
297
- * with its normal UI, shows this, and approves or denies through the API.
401
+ * **A pending MCP authorization, as the app's own consent screen reads it.**
402
+ * `authorize` redirects to the app's `mcp_login_url` with an interaction id,
403
+ * the app authenticates the user with its normal UI, shows this, and approves
404
+ * or denies through the API. An app with no `mcp_login_url` gets the hosted
405
+ * MCP sign-in instead, which reads and decides the same interaction.
298
406
  *
299
407
  * `client_name_verified` is `z.literal(false)`, and that is the whole point of
300
408
  * the field. The name comes from an **unauthenticated** dynamic registration —
@@ -404,5 +512,6 @@ export declare const clientIdentity: z.ZodObject<{
404
512
  app_id: z.ZodNullable<z.ZodUUID>;
405
513
  role_id: z.ZodNullable<z.ZodUUID>;
406
514
  email: z.ZodNullable<z.ZodEmail>;
515
+ two_factor_enabled: z.ZodNullable<z.ZodBoolean>;
407
516
  }, z.core.$strip>;
408
517
  export type ClientIdentity = z.infer<typeof clientIdentity>;
@@ -1,16 +1,17 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  import { z } from 'zod';
3
3
  import { appIdentifier } from './apps.js';
4
- import { APP_USER_DISPLAY_NAME_MAX, providerSlug } from './app-users.js';
5
- import { password } from './identity.js';
4
+ import { APP_USER_DISPLAY_NAME_MAX, appSignInMethods, providerSlug } from './app-users.js';
5
+ import { loginCode, password, recoveryCode, recoveryCodesList, sessionTokens, totpCode } from './identity.js';
6
6
  /**
7
7
  * **The client auth API: the whole of what an app user's browser talks to.**
8
8
  *
9
- * Fleetless shows an app user **no page**. The developer's own UI owns
10
- * every screen — login, registration, verification, invitation acceptance,
9
+ * The developer's own UI owns every screen it wants to — login, emailed
10
+ * code, second factor, registration, verification, invitation acceptance,
11
11
  * password reset, the provider buttons, the MCP consent — and calls these
12
- * routes as JSON. The hosted, app-branded login and consent pages this file
13
- * used to describe are deleted.
12
+ * routes as JSON. **Where the app sets no URL, a Fleetless-hosted page on the
13
+ * auth portal stands in** (`appAuthConfig.hosted_pages`) and calls the same
14
+ * policy: one set of rules, whichever page drives it.
14
15
  *
15
16
  * Everything here is public: `app_identifier` travels in the body (in the
16
17
  * query for a GET), CORS is answered only for the app's `allowed_origins`, and
@@ -27,12 +28,19 @@ import { password } from './identity.js';
27
28
  * the SDK and the cloud cannot drift into disagreeing about it.
28
29
  *
29
30
  * **The enumeration discipline is deliberate, not a preference:**
30
- * `register`, `resend-verification` and `password/reset` answer `202` for every
31
- * policy-allowed request whether or not the address exists, and `login` answers
32
- * the identical `invalid_credentials` for a wrong password, a `blocked` account
33
- * and a `pending_verification` one. Policy refusals are honest —
34
- * `registration_closed` and `domain_not_allowed` say what they are, because
35
- * neither reveals whether a *person* exists.
31
+ * `register`, `resend-verification`, `password/reset` and `login/code` answer
32
+ * `202` for every policy-allowed request whether or not the address exists,
33
+ * and `login` answers the identical `invalid_credentials` for a wrong
34
+ * password, a `blocked` account and a `pending_verification` one. Policy
35
+ * refusals are honest — `registration_closed`, `domain_not_allowed` and
36
+ * `method_not_allowed` say what they are, because none reveals whether a
37
+ * *person* exists.
38
+ *
39
+ * **Every step that signs somebody in answers a `clientSignInResult`**:
40
+ * session tokens, or a two-factor challenge when the person has a confirmed
41
+ * authenticator or the app requires one. No path yields a session without the
42
+ * second factor then — except a sign-in through an identity provider, which
43
+ * owns that sign-in and answers tokens as before.
36
44
  */
37
45
  /* ------------------------------------------------------ password login -- */
38
46
  export const clientLoginRequest = z.object({
@@ -70,6 +78,102 @@ export const clientLogoutRequest = z.object({
70
78
  description: 'Any refresh token of the session to end. The whole token family is revoked server-side, so a token stolen before this call stops working too — clearing a client-side store is a gesture, not a revocation. The answer is `204`: a token the server does not recognise gets it too, since that is the end state being asked for.',
71
79
  }),
72
80
  });
81
+ /* -------------------------------------------------- emailed sign-in code -- */
82
+ /**
83
+ * **Asking for a sign-in code.** A six-digit code, mailed, valid ten minutes;
84
+ * a new request expires the previous code for the same address. The answer is
85
+ * `202` whether or not the address names an account — a decoy, like
86
+ * `resend-verification` — so this is no enumeration oracle.
87
+ */
88
+ export const clientLoginCodeRequest = z
89
+ .object({
90
+ app_identifier: appIdentifier.meta({ description: 'The app to sign in to. An identifier no app carries is `404 not_found`; the address is never the subject of a refusal.' }),
91
+ email: z.email().meta({
92
+ description: 'The address to mail the code to, trimmed and compared case-insensitively. `202` whether or not it names an account of this app.',
93
+ }),
94
+ })
95
+ .strict();
96
+ /** **Spending the code.** Five wrong attempts spend it; so does its tenth minute. */
97
+ export const clientLoginCodeVerifyRequest = z
98
+ .object({
99
+ app_identifier: appIdentifier.meta({ description: 'The app the code was requested for.' }),
100
+ email: z.email().meta({ description: 'The address the code was mailed to, as typed when it was requested; trimmed and compared case-insensitively.' }),
101
+ code: loginCode.meta({ description: 'The six digits from the mail, exactly — leading zeros included, no spaces.' }),
102
+ })
103
+ .strict();
104
+ /* ---------------------------------------------------------- two-factor -- */
105
+ /**
106
+ * **A sign-in that needs its second factor first.** No session exists yet:
107
+ * the `challenge` is a short-lived handle, five minutes, that the next call
108
+ * spends.
109
+ *
110
+ * - `two_factor_required` — the person has an authenticator; send a code or
111
+ * a recovery code to `POST /api/client/two-factor/verify`.
112
+ * - `two_factor_setup_required` — the app requires two-factor and the person
113
+ * has none; set one up with `POST /api/client/two-factor/setup` and
114
+ * `…/setup/confirm`, which answers the session.
115
+ */
116
+ export const twoFactorChallenge = z.object({
117
+ status: z.enum(['two_factor_required', 'two_factor_setup_required']).meta({
118
+ description: '`two_factor_required`: ask for the authenticator code. `two_factor_setup_required`: the app requires two-factor and the person has none yet, so set one up before any session exists.',
119
+ }),
120
+ challenge: z.string().min(1).meta({
121
+ description: 'The handle the next step spends. Valid five minutes; afterwards it answers `410 token_spent` and the sign-in starts over.',
122
+ }),
123
+ });
124
+ /**
125
+ * **What every session-minting sign-in step answers**: password login, code
126
+ * verify, and spending an invitation, verification or reset token. Session
127
+ * tokens when the sign-in is complete; a `twoFactorChallenge` when a second
128
+ * factor comes first. Tell them apart by `status`, which only the challenge
129
+ * carries.
130
+ */
131
+ export const clientSignInResult = z.union([sessionTokens, twoFactorChallenge]);
132
+ /** **Answering a `two_factor_required` challenge**, with the authenticator's code or one recovery code — exactly one of the two. */
133
+ export const clientTwoFactorVerifyRequest = z
134
+ .object({
135
+ challenge: z.string().min(1).meta({ description: 'The challenge the sign-in step answered.' }),
136
+ code: totpCode.optional().meta({ description: 'The six-digit code the authenticator shows now. A code already accepted once is refused, so a replay of a seen code does not sign anybody in.' }),
137
+ recovery_code: recoveryCode.optional().meta({ description: 'One of the ten recovery codes, `xxxxx-xxxxx`, in either case. Spent by its use.' }),
138
+ })
139
+ .strict()
140
+ .refine((b) => (b.code === undefined) !== (b.recovery_code === undefined), { message: 'Send exactly one of code and recovery_code.' });
141
+ /**
142
+ * **Starting an authenticator setup.** Either during sign-in, with the
143
+ * `two_factor_setup_required` challenge in the body, or from the app's own
144
+ * account settings, with the app user's bearer and no challenge.
145
+ */
146
+ export const clientTwoFactorSetupRequest = z
147
+ .object({
148
+ challenge: z.string().min(1).optional().meta({
149
+ description: 'The `two_factor_setup_required` challenge, during sign-in. Absent when the call carries the app user\'s bearer instead.',
150
+ }),
151
+ })
152
+ .strict();
153
+ /** **Confirming the setup** with a code from the new authenticator. Only then is it on. */
154
+ export const clientTwoFactorSetupConfirmRequest = z
155
+ .object({
156
+ challenge: z.string().min(1).optional().meta({ description: 'The same challenge as at `setup`, during sign-in; absent with a bearer.' }),
157
+ code: totpCode.meta({ description: 'A code the new authenticator shows now. It proves the secret was copied correctly before anything depends on it.' }),
158
+ })
159
+ .strict();
160
+ /**
161
+ * **The confirmed setup**: the ten recovery codes, shown this once, and a
162
+ * session — during sign-in the first one, from account settings a fresh one,
163
+ * since every other session of the account ends.
164
+ */
165
+ export const clientTwoFactorSetupConfirmResponse = z.object({
166
+ recovery_codes: recoveryCodesList.meta({
167
+ description: 'The ten single-use recovery codes, lowercase, shown once. Any earlier set is void.',
168
+ }),
169
+ session: sessionTokens.meta({ description: 'The session the sign-in was waiting for, or a fresh one for the account settings.' }),
170
+ });
171
+ /** **Turning the authenticator off**, from the app's own account settings. A current code proves the person still holds it. */
172
+ export const clientTwoFactorDisableRequest = z
173
+ .object({
174
+ code: totpCode.meta({ description: 'A code the authenticator shows now.' }),
175
+ })
176
+ .strict();
73
177
  /* ---------------------------------------------- registration and mails -- */
74
178
  /**
75
179
  * **Self-registration** — and the account it creates cannot log in yet.
@@ -98,8 +202,8 @@ export const clientRegisterRequest = z
98
202
  email: z.email().meta({
99
203
  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.',
100
204
  }),
101
- password: password.meta({
102
- 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.',
205
+ password: password.optional().meta({
206
+ description: 'The password for the new account, at least 12 characters. **Required while the app\'s password method is on, refused while it is off** — both as `400 validation_error` naming `password`. An email-code-only app registers people without one.',
103
207
  }),
104
208
  display_name: z.string().min(1).max(APP_USER_DISPLAY_NAME_MAX).nullable().optional().meta({
105
209
  description: 'An optional human name for the account. The developer\'s own UI decides whether to ask for it.',
@@ -124,12 +228,10 @@ export const clientResendVerificationRequest = z
124
228
  })
125
229
  .strict();
126
230
  /**
127
- * Asking for a reset link **as an app user**.
128
- *
129
- * Same act as `passwordResetRequest`, different shape, because the two surfaces
130
- * identify a person differently. A Fleetless user's address is globally unique
131
- * and resolves alone; an app user's is unique only within their app, so the
132
- * pair is what names them.
231
+ * Asking for a reset link **as an app user** — the only password reset
232
+ * there is: Fleetless users hold no password. An app user's address is
233
+ * unique only within their app, so the pair of app and address is what names
234
+ * them.
133
235
  *
134
236
  * The response is identical for a known and an unknown pair — otherwise this
135
237
  * becomes the enumeration oracle the rest of the family is carefully built not
@@ -170,7 +272,9 @@ export const clientAcceptInvitationRequest = z
170
272
  token: z.string().min(1).meta({
171
273
  description: 'The opaque token from the invitation link, valid seven days. Unknown, expired, revoked and already-accepted all answer `410 token_spent`.',
172
274
  }),
173
- password: password.meta({ description: 'The password the new account will use.' }),
275
+ password: password.optional().meta({
276
+ description: 'The password the new account will use, at least 12 characters. **Required while the app\'s password method is on, refused while it is off** — both as `400 validation_error` naming `password`. An email-code-only app accepts invitations without one.',
277
+ }),
174
278
  display_name: z.string().min(1).max(APP_USER_DISPLAY_NAME_MAX).nullable().optional().meta({
175
279
  description: 'An optional name, overriding whatever the invitation pre-filled.',
176
280
  }),
@@ -210,8 +314,8 @@ export const clientProviderListQuery = z
210
314
  })
211
315
  .meta({ description: 'The one parameter of the public provider listing.' });
212
316
  /**
213
- * What the developer's login page needs to draw its provider buttons, and
214
- * **nothing more**. This route is public and unauthenticated: the issuer, the
317
+ * What the developer's login page needs to draw its provider buttons and its
318
+ * password or code fields, and **nothing more**. This route is public and unauthenticated: the issuer, the
215
319
  * client id, the scopes and the linking policy are all management-side facts
216
320
  * that would tell a stranger how the app's federation is configured.
217
321
  *
@@ -225,7 +329,10 @@ export const clientProviderListResponse = z.object({
225
329
  name: z.string().meta({ description: 'What to write on the button, as the developer configured it.' }),
226
330
  }))
227
331
  .meta({
228
- 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.',
332
+ 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 or code sign-in alone.',
333
+ }),
334
+ sign_in_methods: appSignInMethods.meta({
335
+ description: 'Which of password and emailed code the app accepts, so its sign-in page draws the right fields without guessing. The same value the developer set; public, like the provider buttons.',
229
336
  }),
230
337
  });
231
338
  /**
@@ -353,10 +460,11 @@ export const clientOidcErrorCode = z.enum([
353
460
  ]);
354
461
  /* ------------------------------------------------ MCP, delegated login -- */
355
462
  /**
356
- * **A pending MCP authorization, as the app's own consent screen reads it**
357
- * Fleetless renders no page here either: `authorize` redirects to the
358
- * app's `mcp_login_url` with an interaction id, the app authenticates the user
359
- * with its normal UI, shows this, and approves or denies through the API.
463
+ * **A pending MCP authorization, as the app's own consent screen reads it.**
464
+ * `authorize` redirects to the app's `mcp_login_url` with an interaction id,
465
+ * the app authenticates the user with its normal UI, shows this, and approves
466
+ * or denies through the API. An app with no `mcp_login_url` gets the hosted
467
+ * MCP sign-in instead, which reads and decides the same interaction.
360
468
  *
361
469
  * `client_name_verified` is `z.literal(false)`, and that is the whole point of
362
470
  * the field. The name comes from an **unauthenticated** dynamic registration —
@@ -482,4 +590,7 @@ export const clientIdentity = z.object({
482
590
  email: z.email().nullable().meta({
483
591
  description: 'The address of the Fleetless user or app user behind this session, and `null` for a server key, which is not a person.',
484
592
  }),
593
+ two_factor_enabled: z.boolean().nullable().meta({
594
+ description: 'Whether the app user has a confirmed authenticator, so the app\'s account settings can offer to turn it on or off. `null` unless `kind` is `app_user`.',
595
+ }),
485
596
  });
package/dist/config.d.ts CHANGED
@@ -648,9 +648,9 @@ export declare const LOW_BANDWIDTH_DEFAULTS: {
648
648
  */
649
649
  export declare const lowBandwidthSection: z.ZodObject<{
650
650
  mode: z.ZodOptional<z.ZodEnum<{
651
+ off: "off";
651
652
  auto: "auto";
652
653
  on: "on";
653
- off: "off";
654
654
  }>>;
655
655
  enter_lag_ms: z.ZodOptional<z.ZodNumber>;
656
656
  enter_after_s: z.ZodOptional<z.ZodNumber>;
@@ -858,9 +858,9 @@ export declare const robotConfigDoc: z.ZodObject<{
858
858
  }, z.core.$strict>>>;
859
859
  low_bandwidth: z.ZodOptional<z.ZodObject<{
860
860
  mode: z.ZodOptional<z.ZodEnum<{
861
+ off: "off";
861
862
  auto: "auto";
862
863
  on: "on";
863
- off: "off";
864
864
  }>>;
865
865
  enter_lag_ms: z.ZodOptional<z.ZodNumber>;
866
866
  enter_after_s: z.ZodOptional<z.ZodNumber>;
package/dist/errors.d.ts CHANGED
@@ -63,10 +63,19 @@ export declare const cancelRejectedDetails: z.ZodObject<{
63
63
  }, z.core.$strip>>;
64
64
  }, z.core.$strip>;
65
65
  export type CancelRejectedDetails = z.infer<typeof cancelRejectedDetails>;
66
+ /**
67
+ * The `details` of an `invalid_code` refusal: how many wrong codes are left
68
+ * before the code is spent. Pinned for the reason `parameterInvalidDetails`
69
+ * is — a sign-in page parses it instead of reading the shape from prose.
70
+ */
71
+ export declare const invalidCodeDetails: z.ZodObject<{
72
+ attempts_left: z.ZodNumber;
73
+ }, z.core.$strip>;
74
+ export type InvalidCodeDetails = z.infer<typeof invalidCodeDetails>;
66
75
  /**
67
76
  * The codes in use today. The wire deliberately allows any string — this
68
77
  * list is the shared vocabulary, not a closed set, so a new refusal never
69
78
  * needs a contracts release before it can be reported honestly.
70
79
  */
71
- export declare const ERROR_CODES: readonly ["not_found", "validation_error", "bad_request", "unknown_datapoint", "invalid_token", "protocol_mismatch", "bridge_too_old", "invalid_frame", "duplicate_slug", "reserved_slug", "unknown_slug", "unknown_field_path", "unknown_type", "unknown_topic", "invalid_rate", "invalid_range", "config_conflict", "no_data", "robot_offline", "bridge_timeout", "unauthorized", "forbidden", "invalid_credentials", "token_expired", "token_revoked", "email_taken", "identifier_taken", "weak_password", "account_blocked", "busy", "parameter_invalid", "cancel_rejected", "job_lost", "job_unknown_to_bridge", "action_server_lost", "action_failed", "goal_rejected", "goal_send_failed", "result_failed", "goal_uncontrollable", "bridge_disconnected", "config_changed", "publisher_busy", "unknown_command", "not_subscribable", "camera_offline", "no_snapshot_yet", "live_unavailable", "wrong_kind", "not_recorded", "not_aggregatable", "quota_exceeded", "credential_in_use", "goal_timeout", "robot_in_use", "robot_deletion_partial", "job_queue_full", "invalid_uuid", "rate_limited", "tier_required", "token_spent", "service_timeout", "asset_missing", "dynamic_registration_disabled", "client_limit_reached", "idp_unavailable", "mcp_disabled", "tool_not_available", "capability_required", "last_owner", "role_name_taken", "role_in_use", "last_role", "target_state_conflict", "signup_closed", "draft_not_a_document", "internal_error", "not_cancellable", "unsupported_media_type", "wrong_browser", "invalid_yaml", "unstorable_yaml", "registration_closed", "domain_not_allowed", "email_unverified", "origin_not_allowed", "template_invalid", "provider_disabled", "provider_misconfigured", "invalid_redirect_uri", "interaction_expired"];
80
+ export declare const ERROR_CODES: readonly ["not_found", "validation_error", "bad_request", "unknown_datapoint", "invalid_token", "protocol_mismatch", "bridge_too_old", "invalid_frame", "duplicate_slug", "reserved_slug", "unknown_slug", "unknown_field_path", "unknown_type", "unknown_topic", "invalid_rate", "invalid_range", "config_conflict", "no_data", "robot_offline", "bridge_timeout", "unauthorized", "forbidden", "invalid_credentials", "token_expired", "token_revoked", "email_taken", "identifier_taken", "weak_password", "account_blocked", "busy", "parameter_invalid", "cancel_rejected", "job_lost", "job_unknown_to_bridge", "action_server_lost", "action_failed", "goal_rejected", "goal_send_failed", "result_failed", "goal_uncontrollable", "bridge_disconnected", "config_changed", "publisher_busy", "unknown_command", "not_subscribable", "camera_offline", "no_snapshot_yet", "live_unavailable", "wrong_kind", "not_recorded", "not_aggregatable", "quota_exceeded", "credential_in_use", "goal_timeout", "robot_in_use", "robot_deletion_partial", "job_queue_full", "invalid_uuid", "rate_limited", "tier_required", "token_spent", "service_timeout", "asset_missing", "dynamic_registration_disabled", "client_limit_reached", "idp_unavailable", "mcp_disabled", "tool_not_available", "capability_required", "last_owner", "target_state_conflict", "signup_closed", "draft_not_a_document", "internal_error", "not_cancellable", "unsupported_media_type", "wrong_browser", "invalid_yaml", "unstorable_yaml", "registration_closed", "domain_not_allowed", "email_unverified", "origin_not_allowed", "template_invalid", "provider_disabled", "provider_misconfigured", "invalid_redirect_uri", "interaction_expired", "invalid_code", "method_not_allowed"];
72
81
  export type ErrorCode = (typeof ERROR_CODES)[number];
package/dist/errors.js CHANGED
@@ -53,6 +53,16 @@ export const parameterInvalidDetails = z.object({
53
53
  export const cancelRejectedDetails = z.object({
54
54
  goals: z.array(bridgeCancelResultEntry).min(1),
55
55
  });
56
+ /**
57
+ * The `details` of an `invalid_code` refusal: how many wrong codes are left
58
+ * before the code is spent. Pinned for the reason `parameterInvalidDetails`
59
+ * is — a sign-in page parses it instead of reading the shape from prose.
60
+ */
61
+ export const invalidCodeDetails = z.object({
62
+ attempts_left: z.number().int().min(0).meta({
63
+ description: 'How many more wrong codes this code or challenge takes before it is spent. `0` means the next attempt answers `410 token_spent`.',
64
+ }),
65
+ });
56
66
  /**
57
67
  * The codes in use today. The wire deliberately allows any string — this
58
68
  * list is the shared vocabulary, not a closed set, so a new refusal never
@@ -390,6 +400,10 @@ export const ERROR_CODES = [
390
400
  * Deliberately one code for both: distinguishing them tells a stranger
391
401
  * whether a token ever existed, and the recovery is identical either way —
392
402
  * ask for a new link.
403
+ *
404
+ * The same code answers an emailed sign-in code that is spent, expired or
405
+ * out of attempts, and a two-factor challenge past its five minutes: the
406
+ * recovery is the same — ask for a new code, or start the sign-in again.
393
407
  */
394
408
  'token_spent',
395
409
  // The command path, continued.
@@ -562,35 +576,6 @@ export const ERROR_CODES = [
562
576
  * caller could infer from a silence about existence.
563
577
  */
564
578
  'last_owner',
565
- // App roles.
566
- /**
567
- * **Another role of this app already has that name.** 409, on
568
- * `PATCH /api/apps/:id/roles/:roleId`. Names are unique per app, compared
569
- * exactly as stored after trimming.
570
- *
571
- * Not `validation_error`: the name is well-formed, and the remedy — pick
572
- * another — depends on the app's other roles, not on the body.
573
- */
574
- 'role_name_taken',
575
- /**
576
- * **The role is still held**, by app users, by pending invitations, or as
577
- * the app's default role. 409, on `DELETE /api/apps/:id/roles/:roleId`
578
- * without `move_to`. `details` is `roleInUseDetails`:
579
- * `{ users, invitations, is_default }`.
580
- *
581
- * Not `conflict`: the details tell the console what to offer — a role to
582
- * move them to — and a generic code would leave it guessing.
583
- */
584
- 'role_in_use',
585
- /**
586
- * **An app must keep at least one role.** 409, on
587
- * `DELETE /api/apps/:id/roles/:roleId` for the app's only role: every app
588
- * user holds exactly one role, so an app without roles could hold no users.
589
- *
590
- * The same shape of refusal as `last_owner`: the caller may delete roles;
591
- * the app's remaining state is what refuses this one.
592
- */
593
- 'last_role',
594
579
  // OIDC federation.
595
580
  /**
596
581
  * **The target is in a state that refuses the operation** — not the caller's
@@ -626,11 +611,9 @@ export const ERROR_CODES = [
626
611
  'target_state_conflict',
627
612
  // 2026-09-04 — the public site (closed beta).
628
613
  /**
629
- * `403` from `POST /api/auth/signup` and the portal's sign-up pages while
630
- * `SIGNUP_MODE=closed`. Not `forbidden`: nothing about the caller is
631
- * refused, the door is closed for everyone. The message names the
632
- * waiting list. Produced by cloud `routes/auth.ts` and
633
- * `routes/console-oauth.ts` in the same release.
614
+ * `403` from the portal's sign-up pages while `SIGNUP_MODE=closed`. Not
615
+ * `forbidden`: nothing about the caller is refused, the door is closed for
616
+ * everyone. The message names the waiting list.
634
617
  */
635
618
  'signup_closed',
636
619
  // Emitted by the cloud, catalogued late.
@@ -863,4 +846,21 @@ export const ERROR_CODES = [
863
846
  * advice for the state a person is actually in.
864
847
  */
865
848
  'interaction_expired',
849
+ // 2026-09-30 — email codes and two-factor.
850
+ /**
851
+ * `400`: a six-digit code, an authenticator code or a recovery code that
852
+ * does not match. `details` is `invalidCodeDetails`: how many attempts the
853
+ * emailed code has left, so the page can say so. At zero the code is spent
854
+ * and the next answer is `410 token_spent`. Distinct from
855
+ * `invalid_credentials`, which is about a password and says nothing about
856
+ * attempts.
857
+ */
858
+ 'invalid_code',
859
+ /**
860
+ * `403`: the app does not offer this sign-in method — a password login on a
861
+ * code-only app, a code request on a password-only one, or a password where
862
+ * the app takes none. It names the app's policy, never a person, so it is
863
+ * no enumeration oracle.
864
+ */
865
+ 'method_not_allowed',
866
866
  ];