@fleetless/contracts 5.2.0 → 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.
- package/CHANGELOG.md +99 -0
- package/artifacts/openapi.json +1975 -415
- package/artifacts/routes.json +1697 -251
- package/artifacts/schema/accept-team-invite-request.schema.json +13 -7
- package/artifacts/schema/app-auth-config.schema.json +108 -4
- package/artifacts/schema/app-hosted-pages.schema.json +33 -0
- package/artifacts/schema/app-invitation.schema.json +2 -2
- package/artifacts/schema/app-mail-template-list-response.schema.json +4 -3
- package/artifacts/schema/app-mail-template.schema.json +3 -2
- package/artifacts/schema/app-sign-in-methods.schema.json +19 -0
- package/artifacts/schema/app-user-list-response.schema.json +36 -0
- package/artifacts/schema/app-user.schema.json +36 -0
- package/artifacts/schema/auth-me-response.schema.json +27 -0
- package/artifacts/schema/auth-ok.schema.json +13 -1
- package/artifacts/schema/client-accept-invitation-request.schema.json +3 -4
- package/artifacts/schema/client-identity.schema.json +13 -1
- package/artifacts/schema/client-login-code-request.schema.json +24 -0
- package/artifacts/schema/client-login-code-verify-request.schema.json +30 -0
- package/artifacts/schema/client-provider-list-response.schema.json +22 -2
- package/artifacts/schema/client-register-request.schema.json +3 -4
- package/artifacts/schema/client-sign-in-result.schema.json +55 -0
- package/artifacts/schema/client-two-factor-disable-request.schema.json +15 -0
- package/artifacts/schema/client-two-factor-setup-confirm-request.schema.json +20 -0
- package/artifacts/schema/client-two-factor-setup-confirm-response.schema.json +49 -0
- package/artifacts/schema/client-two-factor-setup-request.schema.json +12 -0
- package/artifacts/schema/client-two-factor-verify-request.schema.json +25 -0
- package/artifacts/schema/create-app-invitation-request.schema.json +1 -1
- package/artifacts/schema/create-passkey-request.schema.json +25 -0
- package/artifacts/schema/create-passkey-response.schema.json +84 -0
- package/artifacts/schema/developer-passkey.schema.json +56 -0
- package/artifacts/schema/developer-two-factor.schema.json +105 -0
- package/artifacts/schema/fleetless-user-list-response.schema.json +22 -0
- package/artifacts/schema/fleetless-user.schema.json +22 -0
- package/artifacts/schema/invalid-code-details.schema.json +16 -0
- package/artifacts/schema/org.schema.json +5 -0
- package/artifacts/schema/patch-org-request.schema.json +5 -3
- package/artifacts/schema/patch-org-response.schema.json +5 -0
- package/artifacts/schema/put-app-auth-look-request.schema.json +22 -0
- package/artifacts/schema/put-app-auth-mcp-request.schema.json +1 -14
- package/artifacts/schema/put-app-auth-sign-in-request.schema.json +39 -0
- package/artifacts/schema/put-app-auth-urls-request.schema.json +31 -4
- package/artifacts/schema/recovery-codes-response.schema.json +20 -0
- package/artifacts/schema/rename-passkey-request.schema.json +16 -0
- package/artifacts/schema/totp-confirm-request.schema.json +15 -0
- package/artifacts/schema/totp-confirm-response.schema.json +27 -0
- package/artifacts/schema/two-factor-challenge.schema.json +24 -0
- package/artifacts/schema/two-factor-setup-response.schema.json +21 -0
- package/artifacts/schema/webauthn-options-response.schema.json +18 -0
- package/dist/app-users.d.ts +154 -34
- package/dist/app-users.js +177 -45
- package/dist/client-auth.d.ts +133 -24
- package/dist/client-auth.js +139 -28
- package/dist/config.d.ts +2 -2
- package/dist/errors.d.ts +10 -1
- package/dist/errors.js +34 -5
- package/dist/identity.d.ts +172 -123
- package/dist/identity.js +189 -101
- package/dist/index.d.ts +9 -9
- package/dist/index.js +11 -7
- package/dist/protocol.d.ts +1 -1
- package/dist/realtime.d.ts +1 -0
- package/dist/rest.d.ts +2 -2
- package/dist/rest.js +17 -28
- package/dist/routes.d.ts +19 -0
- package/dist/routes.js +497 -183
- package/package.json +1 -1
- package/artifacts/schema/developer-login-request.schema.json +0 -19
- package/artifacts/schema/password-reset-confirm.schema.json +0 -19
- package/artifacts/schema/password-reset-request.schema.json +0 -14
- package/artifacts/schema/sign-up-request.schema.json +0 -26
- package/artifacts/schema/sign-up-response.schema.json +0 -127
package/dist/app-users.js
CHANGED
|
@@ -15,19 +15,22 @@ import { idpIssuer, mailStatus, password } from './identity.js';
|
|
|
15
15
|
* So there are now **two identity spaces and nothing joins them**:
|
|
16
16
|
*
|
|
17
17
|
* - *Fleetless users* (`identity.ts`) — the org's team. Email globally unique,
|
|
18
|
-
* tier `owner | developer`,
|
|
18
|
+
* tier `owner | developer`, sign-in by emailed code or passkey, console
|
|
19
|
+
* access.
|
|
19
20
|
* - *app users* (this file) — one app each. Email unique **per app**,
|
|
20
21
|
* case-insensitively. The same address may exist in several apps of one org
|
|
21
22
|
* as unrelated accounts, and a Fleetless user who wants to use an app
|
|
22
23
|
* registers or is invited like anybody else.
|
|
23
24
|
*
|
|
24
|
-
* **
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
25
|
+
* **The developer's own UI owns every screen it wants to own**, and calls the
|
|
26
|
+
* JSON client-auth API (`client-auth.ts`). Fleetless mails a link, and the
|
|
27
|
+
* link points into the app — at the four URLs on `appAuthConfig`. **A URL the
|
|
28
|
+
* app leaves unset falls back to a Fleetless-hosted page** on the auth portal
|
|
29
|
+
* (`hosted_pages`), carrying the app's name, its optional logo and accent
|
|
30
|
+
* colour, so an app works from its first minute: with no web UI of its own,
|
|
31
|
+
* and while its pages point nowhere yet. A set URL always wins. The hosted
|
|
32
|
+
* pages cover mailed links and MCP sign-in only; they are not a hosted login
|
|
33
|
+
* for the app's own web UI.
|
|
31
34
|
*/
|
|
32
35
|
/** App-user display names share the Fleetless-user bound, so a rename cannot be legal in one space and refused in the other. */
|
|
33
36
|
export const APP_USER_DISPLAY_NAME_MAX = 120;
|
|
@@ -103,6 +106,21 @@ export const appUser = z.object({
|
|
|
103
106
|
last_login_at: z.iso.datetime().nullable().meta({
|
|
104
107
|
description: 'When this user last signed in, or `null` if they never have. Required and nullable rather than optional, so *never logged in* stays distinguishable from *this field was not loaded*.',
|
|
105
108
|
}),
|
|
109
|
+
two_factor: z
|
|
110
|
+
.object({
|
|
111
|
+
enabled: z.boolean().meta({
|
|
112
|
+
description: 'Whether the account has a confirmed authenticator app (TOTP). When it has, every sign-in that yields a session asks for a code as well — whatever the app\'s policy — except a sign-in through an identity provider, which owns that sign-in.',
|
|
113
|
+
}),
|
|
114
|
+
enabled_at: z.iso.datetime().nullable().meta({
|
|
115
|
+
description: 'When the authenticator was confirmed, or `null` while `enabled` is `false`.',
|
|
116
|
+
}),
|
|
117
|
+
recovery_codes_left: z.number().int().min(0).max(10).meta({
|
|
118
|
+
description: 'How many of the ten single-use recovery codes are still unspent. `0` while `enabled` is `false`.',
|
|
119
|
+
}),
|
|
120
|
+
})
|
|
121
|
+
.meta({
|
|
122
|
+
description: 'The account\'s second factor, as a developer\'s user list shows it. No secret and no code travels here; resetting it is `DELETE /api/apps/:id/users/:userId/two-factor`.',
|
|
123
|
+
}),
|
|
106
124
|
created_at: z.iso.datetime().meta({
|
|
107
125
|
description: 'When the account was created, as an ISO 8601 timestamp.',
|
|
108
126
|
}),
|
|
@@ -183,19 +201,18 @@ export const createAppInvitationRequest = z
|
|
|
183
201
|
description: 'An optional name to pre-fill the account with; the invitee can change it afterwards.',
|
|
184
202
|
}),
|
|
185
203
|
send_mail: z.boolean().meta({
|
|
186
|
-
description: 'Whether Fleetless mails the invitation.
|
|
204
|
+
description: 'Whether Fleetless mails the invitation. The link points at the app\'s `invite_url`, or at the Fleetless-hosted invitation page when the app has configured none — so the mail always leads somewhere, and nothing is refused for a missing URL.',
|
|
187
205
|
}),
|
|
188
206
|
})
|
|
189
207
|
.strict();
|
|
190
208
|
/**
|
|
191
209
|
* The invitation as issued.
|
|
192
210
|
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* Fleetless-hosted URL would name a page this product does not serve.
|
|
211
|
+
* The link points into the developer's app, at their configured `invite_url`,
|
|
212
|
+
* or at the Fleetless-hosted invitation page (`appAuthConfig.hosted_pages`)
|
|
213
|
+
* when the app has configured none. **`accept_url` stays nullable** so a
|
|
214
|
+
* reader written against the earlier shape keeps parsing; the cloud fills it
|
|
215
|
+
* in every case now that a hosted page always exists.
|
|
199
216
|
*/
|
|
200
217
|
export const appInvitation = z.object({
|
|
201
218
|
id: z.uuid().meta({ description: 'The invitation, as listed and revoked by the developer.' }),
|
|
@@ -204,10 +221,10 @@ export const appInvitation = z.object({
|
|
|
204
221
|
role_id: z.uuid().meta({ description: 'The role the invitee holds once they accept. Resolved at creation, so a later change to the app\'s default role does not silently re-aim an outstanding invitation.' }),
|
|
205
222
|
expires_at: z.iso.datetime().meta({ description: 'When the token stops working. Seven days from issue; an expired token answers exactly as an unknown one does.' }),
|
|
206
223
|
accept_url: z.url().max(500).nullable().meta({
|
|
207
|
-
description: 'The link to give the invitee
|
|
224
|
+
description: 'The link to give the invitee: the app\'s `invite_url` with the token substituted for `{token}`, or the Fleetless-hosted invitation page when the app has configured none. Bounded like every other URL that gets mailed, logged and rendered. Nullable for readers of the earlier shape; the cloud always fills it.',
|
|
208
225
|
}),
|
|
209
226
|
mail: mailStatus.meta({
|
|
210
|
-
description: 'What happened to the mail: `sent` means the SMTP server accepted it, not that it was delivered; `not_requested` means none was attempted
|
|
227
|
+
description: 'What happened to the mail: `sent` means the SMTP server accepted it, not that it was delivered; `not_requested` means none was attempted because the caller asked for none; `not_configured` is an expected state and not a failure; `failed` is the one worth somebody\'s attention.',
|
|
211
228
|
}),
|
|
212
229
|
});
|
|
213
230
|
/**
|
|
@@ -435,6 +452,77 @@ export const emailDomain = z
|
|
|
435
452
|
.min(1)
|
|
436
453
|
.max(253)
|
|
437
454
|
.regex(/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/, 'must be a lowercase domain name with at least two labels');
|
|
455
|
+
/**
|
|
456
|
+
* **How an app's users sign in: by password, by emailed code, or both.**
|
|
457
|
+
*
|
|
458
|
+
* At least one is on — an app with neither would have no door but its
|
|
459
|
+
* identity providers, and a provider can be disabled. Identity providers are
|
|
460
|
+
* not part of this choice; they stay on top of whichever methods are on. A
|
|
461
|
+
* method turned off refuses its routes with `method_not_allowed`; a stored
|
|
462
|
+
* password stays stored, so turning the method back on restores it.
|
|
463
|
+
*/
|
|
464
|
+
export const appSignInMethods = z
|
|
465
|
+
.object({
|
|
466
|
+
password: z.boolean().meta({
|
|
467
|
+
description: 'Whether app users may sign in with a password. Off refuses `POST /api/client/login` with `method_not_allowed`, and registration and invitations then take no password.',
|
|
468
|
+
}),
|
|
469
|
+
email_code: z.boolean().meta({
|
|
470
|
+
description: 'Whether app users may sign in with a six-digit code mailed to them, valid ten minutes. A code needs no URL, so it works in local development and in an app with no web UI.',
|
|
471
|
+
}),
|
|
472
|
+
})
|
|
473
|
+
.strict()
|
|
474
|
+
.refine((m) => m.password || m.email_code, { message: 'At least one sign-in method must be on.', path: ['password'] });
|
|
475
|
+
/**
|
|
476
|
+
* **Whether an app asks its users for a second factor**, an authenticator
|
|
477
|
+
* app (TOTP) with ten single-use recovery codes.
|
|
478
|
+
*
|
|
479
|
+
* - `off` — nobody is asked to set one up, and nobody can.
|
|
480
|
+
* - `optional` — people turn it on in the app's own account settings.
|
|
481
|
+
* - `required` — a person without one sets it up at their next sign-in,
|
|
482
|
+
* before any session exists. Nobody is signed out when it is switched on.
|
|
483
|
+
*
|
|
484
|
+
* Whatever the policy, a person who **has** a confirmed authenticator is asked
|
|
485
|
+
* for a code at every sign-in that yields a session. A sign-in through an
|
|
486
|
+
* identity provider is never asked: the provider owns that sign-in.
|
|
487
|
+
*/
|
|
488
|
+
export const appTwoFactorPolicy = z.enum(['off', 'optional', 'required']);
|
|
489
|
+
/**
|
|
490
|
+
* **The app's own home page**: https, or http on `localhost`/`127.0.0.1` —
|
|
491
|
+
* the host rule `appUrlTemplate` keeps — and no placeholder, because nothing
|
|
492
|
+
* is substituted into it. The hosted pages link to it as `Open <app>` when a
|
|
493
|
+
* flow is done.
|
|
494
|
+
*/
|
|
495
|
+
export const appHomeUrl = z
|
|
496
|
+
.url()
|
|
497
|
+
.max(500)
|
|
498
|
+
.refine((v) => {
|
|
499
|
+
try {
|
|
500
|
+
const u = new URL(v);
|
|
501
|
+
const hostOk = u.protocol === 'https:' || (u.protocol === 'http:' && ['localhost', '127.0.0.1'].includes(u.hostname));
|
|
502
|
+
return hostOk && !v.includes('{');
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
}, { message: 'An https URL (http only on localhost) without a placeholder.' });
|
|
508
|
+
/** The accent colour of the hosted pages: `#rrggbb`, lowercase, the one spelling the renderer compares against. */
|
|
509
|
+
export const hostedAccent = z.string().regex(/^#[0-9a-f]{6}$/, 'must be a lowercase hex colour, #rrggbb');
|
|
510
|
+
/** The largest logo the hosted pages take, in bytes: 100 KB. */
|
|
511
|
+
export const HOSTED_LOGO_MAX_BYTES = 102_400;
|
|
512
|
+
/** The logo types the hosted pages take. An SVG is served sandboxed and embedded only as an image. */
|
|
513
|
+
export const HOSTED_LOGO_TYPES = ['image/png', 'image/svg+xml'];
|
|
514
|
+
/**
|
|
515
|
+
* **The Fleetless-hosted pages an unset URL falls back to**, one per URL
|
|
516
|
+
* field, as templates with the same placeholder the field takes. Read-only:
|
|
517
|
+
* they are minted by the cloud from the auth portal's base URL and the app's
|
|
518
|
+
* identifier.
|
|
519
|
+
*/
|
|
520
|
+
export const appHostedPages = z.object({
|
|
521
|
+
invite_url: z.url().meta({ description: 'The hosted invitation page, `<portal>/app/<identifier>/invite/{token}`.' }),
|
|
522
|
+
verify_url: z.url().meta({ description: 'The hosted email-confirmation page, `<portal>/app/<identifier>/verify/{token}`.' }),
|
|
523
|
+
reset_url: z.url().meta({ description: 'The hosted new-password page, `<portal>/app/<identifier>/reset/{token}`.' }),
|
|
524
|
+
mcp_login_url: z.url().meta({ description: 'The hosted MCP sign-in, `<portal>/app/<identifier>/mcp/{interaction}`.' }),
|
|
525
|
+
});
|
|
438
526
|
/**
|
|
439
527
|
* **The app's auth settings: one row per app, configured by a Fleetless user.**
|
|
440
528
|
*
|
|
@@ -444,11 +532,11 @@ export const emailDomain = z
|
|
|
444
532
|
* a developer inviting somebody by hand has already made the decision the
|
|
445
533
|
* whitelist automates.
|
|
446
534
|
*
|
|
447
|
-
* The four URLs
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
535
|
+
* The four URLs point Fleetless's mails and the MCP sign-in into the
|
|
536
|
+
* developer's app. **Each is optional**: an unset one falls back to the
|
|
537
|
+
* Fleetless-hosted page in `hosted_pages`, so nothing is refused for a
|
|
538
|
+
* missing URL — self-registration, mailed invitations, resets and MCP sign-in
|
|
539
|
+
* all work before the app has a page of its own.
|
|
452
540
|
*/
|
|
453
541
|
export const appAuthConfig = z.object({
|
|
454
542
|
self_registration: z.boolean().meta({
|
|
@@ -464,16 +552,34 @@ export const appAuthConfig = z.object({
|
|
|
464
552
|
description: 'Whether this app serves an MCP endpoint at `/mcp/<identifier>`. Off refuses the whole OAuth surface for the app, not merely the tool calls, and is re-read on every request rather than cached off a token.',
|
|
465
553
|
}),
|
|
466
554
|
invite_url: appUrlTemplate('{token}').nullable().meta({
|
|
467
|
-
description: 'The page in the developer\'s app that accepts an invitation, with `{token}` where the token goes. `null`
|
|
555
|
+
description: 'The page in the developer\'s app that accepts an invitation, with `{token}` where the token goes. `null` means the hosted page in `hosted_pages` is used.',
|
|
468
556
|
}),
|
|
469
557
|
verify_url: appUrlTemplate('{token}').nullable().meta({
|
|
470
|
-
description: 'The page that confirms a new address, with `{token}` where the token goes.
|
|
558
|
+
description: 'The page that confirms a new address, with `{token}` where the token goes. `null` means the hosted page in `hosted_pages` is used.',
|
|
471
559
|
}),
|
|
472
560
|
reset_url: appUrlTemplate('{token}').nullable().meta({
|
|
473
|
-
description: 'The page that takes a new password, with `{token}` where the token goes.',
|
|
561
|
+
description: 'The page that takes a new password, with `{token}` where the token goes. `null` means the hosted page in `hosted_pages` is used.',
|
|
474
562
|
}),
|
|
475
563
|
mcp_login_url: appUrlTemplate('{interaction}').nullable().meta({
|
|
476
|
-
description: 'The page an MCP authorization redirects to, with `{interaction}` where the interaction id goes. Not a token: the id names a pending request the server already holds, and the app authenticates the user itself before approving it.',
|
|
564
|
+
description: 'The page an MCP authorization redirects to, with `{interaction}` where the interaction id goes. Not a token: the id names a pending request the server already holds, and the app authenticates the user itself before approving it. `null` means the hosted MCP sign-in in `hosted_pages` is used.',
|
|
565
|
+
}),
|
|
566
|
+
app_url: appHomeUrl.nullable().meta({
|
|
567
|
+
description: 'The app\'s own home page, linked as `Open <app>` when a hosted flow is done. `null` makes the hosted done page say `You can close this tab`.',
|
|
568
|
+
}),
|
|
569
|
+
sign_in_methods: appSignInMethods.meta({
|
|
570
|
+
description: 'Which sign-in methods the app offers: password, emailed code, or both — at least one. Identity providers stay on top of either. The default is password only.',
|
|
571
|
+
}),
|
|
572
|
+
two_factor: appTwoFactorPolicy.meta({
|
|
573
|
+
description: 'Whether the app asks for an authenticator code: `off` (the default), `optional` or `required`. A person with a confirmed authenticator is asked at every sign-in whatever the policy; a sign-in through an identity provider is never asked.',
|
|
574
|
+
}),
|
|
575
|
+
hosted_logo_url: z.url().nullable().meta({
|
|
576
|
+
description: 'Where the hosted pages load the app\'s logo from, `<portal>/app/<identifier>/logo`, or `null` when no logo is stored. **Read-only** — the logo is written through `PUT /api/apps/:id/auth-config/logo`.',
|
|
577
|
+
}),
|
|
578
|
+
hosted_accent: hostedAccent.nullable().meta({
|
|
579
|
+
description: 'The accent colour of the hosted pages, `#rrggbb` in lowercase, or `null` for the neutral shell\'s own.',
|
|
580
|
+
}),
|
|
581
|
+
hosted_pages: appHostedPages.meta({
|
|
582
|
+
description: 'The Fleetless-hosted pages an unset URL falls back to, as templates. **Read-only**: minted by the cloud from the auth portal and the app\'s identifier.',
|
|
477
583
|
}),
|
|
478
584
|
oidc_callback_url: z.url().meta({
|
|
479
585
|
description: 'The one callback URL to register at every identity provider, the same for every app and every provider. **Read-only** — it is minted by the cloud from its own public base URL, and a writable version of this field would let a caller point the return leg, which carries an authorization code, at a host they own.',
|
|
@@ -484,8 +590,8 @@ export const appAuthConfig = z.object({
|
|
|
484
590
|
* `PUT /api/apps/:id/auth-config/registration` — who may get in, and from
|
|
485
591
|
* where.
|
|
486
592
|
*
|
|
487
|
-
*
|
|
488
|
-
* every field of its slice required:
|
|
593
|
+
* Several slices rather than one document, and each still a **replace** with
|
|
594
|
+
* every field of its slice required: several screens carving up one
|
|
489
595
|
* all-required request is how a field nobody's screen shows becomes a field
|
|
490
596
|
* somebody's save clears. The slice states its own ownership, so a new field
|
|
491
597
|
* lands in one schema and one screen.
|
|
@@ -496,25 +602,39 @@ export const appAuthConfig = z.object({
|
|
|
496
602
|
export const putAppAuthRegistrationRequest = appAuthConfig
|
|
497
603
|
.pick({ self_registration: true, allowed_domains: true, allowed_origins: true })
|
|
498
604
|
.strict();
|
|
499
|
-
/** `PUT /api/apps/:id/auth-config/
|
|
605
|
+
/** `PUT /api/apps/:id/auth-config/sign-in` — how the app's users sign in, and whether they give a second factor. */
|
|
606
|
+
export const putAppAuthSignInRequest = appAuthConfig
|
|
607
|
+
.pick({ sign_in_methods: true, two_factor: true })
|
|
608
|
+
.strict();
|
|
609
|
+
/**
|
|
610
|
+
* `PUT /api/apps/:id/auth-config/urls` — the app's home page and the four
|
|
611
|
+
* pages Fleetless's mails and the MCP sign-in point at. One slice, because
|
|
612
|
+
* the console's Pages section owns all five; each `null` falls back to the
|
|
613
|
+
* hosted page.
|
|
614
|
+
*/
|
|
500
615
|
export const putAppAuthUrlsRequest = appAuthConfig
|
|
501
|
-
.pick({ invite_url: true, verify_url: true, reset_url: true })
|
|
616
|
+
.pick({ app_url: true, invite_url: true, verify_url: true, reset_url: true, mcp_login_url: true })
|
|
502
617
|
.strict();
|
|
503
618
|
/**
|
|
504
|
-
* `PUT /api/apps/:id/auth-config/mcp` — the switch
|
|
505
|
-
*
|
|
506
|
-
*
|
|
619
|
+
* `PUT /api/apps/:id/auth-config/mcp` — the switch alone. Its login URL moved
|
|
620
|
+
* to the `urls` slice: on without a URL no longer refuses anything, because
|
|
621
|
+
* the hosted MCP sign-in stands in for it.
|
|
507
622
|
*/
|
|
508
623
|
export const putAppAuthMcpRequest = appAuthConfig
|
|
509
|
-
.pick({ mcp_enabled: true
|
|
624
|
+
.pick({ mcp_enabled: true })
|
|
625
|
+
.strict();
|
|
626
|
+
/** `PUT /api/apps/:id/auth-config/look` — the hosted pages' accent colour. The logo is its own write, a raw image body. */
|
|
627
|
+
export const putAppAuthLookRequest = appAuthConfig
|
|
628
|
+
.pick({ hosted_accent: true })
|
|
510
629
|
.strict();
|
|
511
630
|
/**
|
|
512
|
-
* The
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
631
|
+
* The four mails a developer may replace with their own template: the
|
|
632
|
+
* invitation, the address confirmation, the password reset and the sign-in
|
|
633
|
+
* code. Mails to *Fleetless* users — a team invitation, a console sign-in
|
|
634
|
+
* code — stay Fleetless default and are deliberately not customisable: they
|
|
635
|
+
* are about this platform, not about the developer's product.
|
|
516
636
|
*/
|
|
517
|
-
export const mailTemplateKind = z.enum(['invite', 'verify', 'reset']);
|
|
637
|
+
export const mailTemplateKind = z.enum(['invite', 'verify', 'reset', 'login_code']);
|
|
518
638
|
/**
|
|
519
639
|
* **Every variable a template may name, and the list is closed.**
|
|
520
640
|
*
|
|
@@ -532,9 +652,11 @@ export const MAIL_TEMPLATE_VARIABLES = [
|
|
|
532
652
|
'role.name',
|
|
533
653
|
'link',
|
|
534
654
|
'expires_in_hours',
|
|
655
|
+
'code',
|
|
656
|
+
'expires_in_minutes',
|
|
535
657
|
];
|
|
536
658
|
/**
|
|
537
|
-
* **The Fleetless default text for the
|
|
659
|
+
* **The Fleetless default text for the four app mails.**
|
|
538
660
|
*
|
|
539
661
|
* It lives here rather than in the cloud because two products send the same
|
|
540
662
|
* words: the cloud renders these when an app has no template of its own, and
|
|
@@ -568,12 +690,15 @@ export const MAIL_TEMPLATE_VARIABLES = [
|
|
|
568
690
|
* that one is written by an authenticated developer about somebody they
|
|
569
691
|
* invited.
|
|
570
692
|
*
|
|
571
|
-
* **`expires_in_hours` is the
|
|
693
|
+
* **`expires_in_hours` is the lifetime variable of the three link mails**, and
|
|
572
694
|
* the three values are 1, 24 and 168. "The next 168 hours" is not how a person
|
|
573
695
|
* says a week, so each default converts: 48 and up reads in days, exactly one
|
|
574
696
|
* reads "1 hour", everything else reads in hours. The conversion is in the
|
|
575
697
|
* template rather than in a new variable because a custom template has the
|
|
576
|
-
* same problem and this is the spelling it can copy.
|
|
698
|
+
* same problem and this is the spelling it can copy. The sign-in code mail
|
|
699
|
+
* carries no link: it names the `code` and its lifetime in
|
|
700
|
+
* `expires_in_minutes` (10), and greets nobody, since whoever asked for it
|
|
701
|
+
* typed the address unauthenticated.
|
|
577
702
|
*
|
|
578
703
|
* **What contracts does NOT assert about these.** That they compile as Liquid
|
|
579
704
|
* is the cloud's business — contracts has no renderer and adding one to check
|
|
@@ -588,7 +713,7 @@ export const DEFAULT_MAIL_TEMPLATES = {
|
|
|
588
713
|
|
|
589
714
|
{{ org.name }} has invited you to {{ app.name }} as {{ role.name }}.
|
|
590
715
|
|
|
591
|
-
Accept the invitation
|
|
716
|
+
Accept the invitation:
|
|
592
717
|
{{ link }}
|
|
593
718
|
|
|
594
719
|
The link works for the next {% if expires_in_hours >= 48 %}{{ expires_in_hours | divided_by: 24 }} days{% elsif expires_in_hours == 1 %}1 hour{% else %}{{ expires_in_hours }} hours{% endif %}. If you were not expecting this invitation, ignore this mail — no account is created until you accept.
|
|
@@ -619,6 +744,13 @@ The link works for the next {% if expires_in_hours >= 48 %}{{ expires_in_hours |
|
|
|
619
744
|
`,
|
|
620
745
|
html: null,
|
|
621
746
|
},
|
|
747
|
+
login_code: {
|
|
748
|
+
subject: 'Your {{ app.name }} sign-in code',
|
|
749
|
+
text: `Your sign-in code for {{ app.name }} is {{ code }}.
|
|
750
|
+
|
|
751
|
+
It works once, for {{ expires_in_minutes }} minutes. If you did not ask for it, ignore this mail.`,
|
|
752
|
+
html: null,
|
|
753
|
+
},
|
|
622
754
|
};
|
|
623
755
|
/**
|
|
624
756
|
* One stored template. `html` is nullable because the mailer's HTML part is
|
|
@@ -626,7 +758,7 @@ The link works for the next {% if expires_in_hours >= 48 %}{{ expires_in_hours |
|
|
|
626
758
|
* should not have to write the same words twice.
|
|
627
759
|
*/
|
|
628
760
|
export const appMailTemplate = z.object({
|
|
629
|
-
kind: mailTemplateKind.meta({ description: 'Which of the
|
|
761
|
+
kind: mailTemplateKind.meta({ description: 'Which of the four mails this template replaces.' }),
|
|
630
762
|
subject: z.string().min(1).max(200).meta({ description: 'The subject line, a Liquid template. Bounded because a subject is rendered into a header.' }),
|
|
631
763
|
text: z.string().min(1).max(20_000).meta({ description: 'The plain-text body, a Liquid template. Required even when an HTML part is given: a mail with no text part is unreadable to a client that refuses HTML.' }),
|
|
632
764
|
html: z.string().min(1).max(100_000).nullable().meta({ description: 'The optional HTML body, a Liquid template. `null` means this template is text-only, which is a complete mail and not a half-configured one.' }),
|
|
@@ -634,7 +766,7 @@ export const appMailTemplate = z.object({
|
|
|
634
766
|
});
|
|
635
767
|
/** `GET /api/apps/:id/mail-templates` — **only the kinds that have a custom template.** An absent kind is one using the Fleetless default, which is a state and not a gap. */
|
|
636
768
|
export const appMailTemplateListResponse = z.object({
|
|
637
|
-
templates: z.array(appMailTemplate).max(
|
|
769
|
+
templates: z.array(appMailTemplate).max(4).meta({
|
|
638
770
|
description: 'The app\'s custom templates. A kind that does not appear is one using the Fleetless default text — an ordinary state, not a missing row.',
|
|
639
771
|
}),
|
|
640
772
|
});
|
package/dist/client-auth.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
7
|
-
*
|
|
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.
|
|
10
|
-
*
|
|
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
|
|
28
|
-
* policy-allowed request whether or not the address exists,
|
|
29
|
-
* the identical `invalid_credentials` for a wrong
|
|
30
|
-
* and a `pending_verification` one. Policy
|
|
31
|
-
* `registration_closed
|
|
32
|
-
*
|
|
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
|
-
*
|
|
103
|
-
*
|
|
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
|
|
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
|
-
*
|
|
296
|
-
* app
|
|
297
|
-
*
|
|
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>;
|