@fleetless/contracts 5.3.0-next.1 → 6.0.0-next.2
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 +108 -33
- package/artifacts/openapi.json +2053 -784
- package/artifacts/routes.json +1680 -323
- 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 +5 -12
- 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/audit-query.schema.json +0 -6
- 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/job-actor.schema.json +1 -15
- package/artifacts/schema/job-run-list-response.schema.json +1 -15
- package/artifacts/schema/job-run.schema.json +1 -15
- 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/{role-rename-request.schema.json → rename-passkey-request.schema.json} +2 -2
- package/artifacts/schema/role-list-response.schema.json +1 -1
- package/artifacts/schema/role.schema.json +1 -1
- 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 -35
- package/dist/app-users.js +177 -46
- package/dist/apps.d.ts +2 -38
- package/dist/apps.js +3 -46
- package/dist/audit.d.ts +0 -1
- package/dist/audit.js +0 -13
- 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 +37 -38
- package/dist/identity.d.ts +175 -128
- package/dist/identity.js +192 -106
- package/dist/index.d.ts +11 -13
- package/dist/index.js +12 -10
- package/dist/jobs.d.ts +0 -3
- package/dist/jobs.js +0 -16
- 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 +26 -0
- package/dist/routes.js +528 -234
- package/package.json +1 -1
- package/artifacts/schema/developer-login-request.schema.json +0 -19
- package/artifacts/schema/feedback-request.schema.json +0 -34
- package/artifacts/schema/feedback-response.schema.json +0 -27
- package/artifacts/schema/password-reset-confirm.schema.json +0 -19
- package/artifacts/schema/password-reset-request.schema.json +0 -14
- package/artifacts/schema/role-delete-query.schema.json +0 -13
- package/artifacts/schema/role-in-use-details.schema.json +0 -28
- package/artifacts/schema/sign-up-request.schema.json +0 -26
- package/artifacts/schema/sign-up-response.schema.json +0 -127
- package/dist/feedback.d.ts +0 -42
- package/dist/feedback.js +0 -35
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,17 @@ 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
|
-
* indistinguishable from a mapper that dropped the field and a fabricated
|
|
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 — so there is always a link, and
|
|
214
|
+
* `accept_url` is never `null`.
|
|
199
215
|
*/
|
|
200
216
|
export const appInvitation = z.object({
|
|
201
217
|
id: z.uuid().meta({ description: 'The invitation, as listed and revoked by the developer.' }),
|
|
@@ -203,11 +219,11 @@ export const appInvitation = z.object({
|
|
|
203
219
|
email: z.email().meta({ description: 'The address the invitation was addressed to.' }),
|
|
204
220
|
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
221
|
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
|
-
accept_url: z.url().max(500).
|
|
207
|
-
description: 'The link to give the invitee
|
|
222
|
+
accept_url: z.url().max(500).meta({
|
|
223
|
+
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.',
|
|
208
224
|
}),
|
|
209
225
|
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
|
|
226
|
+
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
227
|
}),
|
|
212
228
|
});
|
|
213
229
|
/**
|
|
@@ -435,6 +451,77 @@ export const emailDomain = z
|
|
|
435
451
|
.min(1)
|
|
436
452
|
.max(253)
|
|
437
453
|
.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');
|
|
454
|
+
/**
|
|
455
|
+
* **How an app's users sign in: by password, by emailed code, or both.**
|
|
456
|
+
*
|
|
457
|
+
* At least one is on — an app with neither would have no door but its
|
|
458
|
+
* identity providers, and a provider can be disabled. Identity providers are
|
|
459
|
+
* not part of this choice; they stay on top of whichever methods are on. A
|
|
460
|
+
* method turned off refuses its routes with `method_not_allowed`; a stored
|
|
461
|
+
* password stays stored, so turning the method back on restores it.
|
|
462
|
+
*/
|
|
463
|
+
export const appSignInMethods = z
|
|
464
|
+
.object({
|
|
465
|
+
password: z.boolean().meta({
|
|
466
|
+
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.',
|
|
467
|
+
}),
|
|
468
|
+
email_code: z.boolean().meta({
|
|
469
|
+
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.',
|
|
470
|
+
}),
|
|
471
|
+
})
|
|
472
|
+
.strict()
|
|
473
|
+
.refine((m) => m.password || m.email_code, { message: 'At least one sign-in method must be on.', path: ['password'] });
|
|
474
|
+
/**
|
|
475
|
+
* **Whether an app asks its users for a second factor**, an authenticator
|
|
476
|
+
* app (TOTP) with ten single-use recovery codes.
|
|
477
|
+
*
|
|
478
|
+
* - `off` — nobody is asked to set one up, and nobody can.
|
|
479
|
+
* - `optional` — people turn it on in the app's own account settings.
|
|
480
|
+
* - `required` — a person without one sets it up at their next sign-in,
|
|
481
|
+
* before any session exists. Nobody is signed out when it is switched on.
|
|
482
|
+
*
|
|
483
|
+
* Whatever the policy, a person who **has** a confirmed authenticator is asked
|
|
484
|
+
* for a code at every sign-in that yields a session. A sign-in through an
|
|
485
|
+
* identity provider is never asked: the provider owns that sign-in.
|
|
486
|
+
*/
|
|
487
|
+
export const appTwoFactorPolicy = z.enum(['off', 'optional', 'required']);
|
|
488
|
+
/**
|
|
489
|
+
* **The app's own home page**: https, or http on `localhost`/`127.0.0.1` —
|
|
490
|
+
* the host rule `appUrlTemplate` keeps — and no placeholder, because nothing
|
|
491
|
+
* is substituted into it. The hosted pages link to it as `Open <app>` when a
|
|
492
|
+
* flow is done.
|
|
493
|
+
*/
|
|
494
|
+
export const appHomeUrl = z
|
|
495
|
+
.url()
|
|
496
|
+
.max(500)
|
|
497
|
+
.refine((v) => {
|
|
498
|
+
try {
|
|
499
|
+
const u = new URL(v);
|
|
500
|
+
const hostOk = u.protocol === 'https:' || (u.protocol === 'http:' && ['localhost', '127.0.0.1'].includes(u.hostname));
|
|
501
|
+
return hostOk && !v.includes('{');
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
}, { message: 'An https URL (http only on localhost) without a placeholder.' });
|
|
507
|
+
/** The accent colour of the hosted pages: `#rrggbb`, lowercase, the one spelling the renderer compares against. */
|
|
508
|
+
export const hostedAccent = z.string().regex(/^#[0-9a-f]{6}$/, 'must be a lowercase hex colour, #rrggbb');
|
|
509
|
+
/** The largest logo the hosted pages take, in bytes: 100 KB. */
|
|
510
|
+
export const HOSTED_LOGO_MAX_BYTES = 102_400;
|
|
511
|
+
/** The logo types the hosted pages take. An SVG is served sandboxed and embedded only as an image. */
|
|
512
|
+
export const HOSTED_LOGO_TYPES = ['image/png', 'image/svg+xml'];
|
|
513
|
+
/**
|
|
514
|
+
* **The Fleetless-hosted pages an unset URL falls back to**, one per URL
|
|
515
|
+
* field, as templates with the same placeholder the field takes. Read-only:
|
|
516
|
+
* they are minted by the cloud from the auth portal's base URL and the app's
|
|
517
|
+
* identifier.
|
|
518
|
+
*/
|
|
519
|
+
export const appHostedPages = z.object({
|
|
520
|
+
invite_url: z.url().meta({ description: 'The hosted invitation page, `<portal>/app/<identifier>/invite/{token}`.' }),
|
|
521
|
+
verify_url: z.url().meta({ description: 'The hosted email-confirmation page, `<portal>/app/<identifier>/verify/{token}`.' }),
|
|
522
|
+
reset_url: z.url().meta({ description: 'The hosted new-password page, `<portal>/app/<identifier>/reset/{token}`.' }),
|
|
523
|
+
mcp_login_url: z.url().meta({ description: 'The hosted MCP sign-in, `<portal>/app/<identifier>/mcp/{interaction}`.' }),
|
|
524
|
+
});
|
|
438
525
|
/**
|
|
439
526
|
* **The app's auth settings: one row per app, configured by a Fleetless user.**
|
|
440
527
|
*
|
|
@@ -444,11 +531,11 @@ export const emailDomain = z
|
|
|
444
531
|
* a developer inviting somebody by hand has already made the decision the
|
|
445
532
|
* whitelist automates.
|
|
446
533
|
*
|
|
447
|
-
* The four URLs
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
534
|
+
* The four URLs point Fleetless's mails and the MCP sign-in into the
|
|
535
|
+
* developer's app. **Each is optional**: an unset one falls back to the
|
|
536
|
+
* Fleetless-hosted page in `hosted_pages`, so nothing is refused for a
|
|
537
|
+
* missing URL — self-registration, mailed invitations, resets and MCP sign-in
|
|
538
|
+
* all work before the app has a page of its own.
|
|
452
539
|
*/
|
|
453
540
|
export const appAuthConfig = z.object({
|
|
454
541
|
self_registration: z.boolean().meta({
|
|
@@ -464,16 +551,34 @@ export const appAuthConfig = z.object({
|
|
|
464
551
|
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
552
|
}),
|
|
466
553
|
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`
|
|
554
|
+
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
555
|
}),
|
|
469
556
|
verify_url: appUrlTemplate('{token}').nullable().meta({
|
|
470
|
-
description: 'The page that confirms a new address, with `{token}` where the token goes.
|
|
557
|
+
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
558
|
}),
|
|
472
559
|
reset_url: appUrlTemplate('{token}').nullable().meta({
|
|
473
|
-
description: 'The page that takes a new password, with `{token}` where the token goes.',
|
|
560
|
+
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
561
|
}),
|
|
475
562
|
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.',
|
|
563
|
+
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.',
|
|
564
|
+
}),
|
|
565
|
+
app_url: appHomeUrl.nullable().meta({
|
|
566
|
+
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`.',
|
|
567
|
+
}),
|
|
568
|
+
sign_in_methods: appSignInMethods.meta({
|
|
569
|
+
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.',
|
|
570
|
+
}),
|
|
571
|
+
two_factor: appTwoFactorPolicy.meta({
|
|
572
|
+
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.',
|
|
573
|
+
}),
|
|
574
|
+
hosted_logo_url: z.url().nullable().meta({
|
|
575
|
+
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`.',
|
|
576
|
+
}),
|
|
577
|
+
hosted_accent: hostedAccent.nullable().meta({
|
|
578
|
+
description: 'The accent colour of the hosted pages, `#rrggbb` in lowercase, or `null` for the neutral shell\'s own.',
|
|
579
|
+
}),
|
|
580
|
+
hosted_pages: appHostedPages.meta({
|
|
581
|
+
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
582
|
}),
|
|
478
583
|
oidc_callback_url: z.url().meta({
|
|
479
584
|
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 +589,8 @@ export const appAuthConfig = z.object({
|
|
|
484
589
|
* `PUT /api/apps/:id/auth-config/registration` — who may get in, and from
|
|
485
590
|
* where.
|
|
486
591
|
*
|
|
487
|
-
*
|
|
488
|
-
* every field of its slice required:
|
|
592
|
+
* Several slices rather than one document, and each still a **replace** with
|
|
593
|
+
* every field of its slice required: several screens carving up one
|
|
489
594
|
* all-required request is how a field nobody's screen shows becomes a field
|
|
490
595
|
* somebody's save clears. The slice states its own ownership, so a new field
|
|
491
596
|
* lands in one schema and one screen.
|
|
@@ -496,25 +601,39 @@ export const appAuthConfig = z.object({
|
|
|
496
601
|
export const putAppAuthRegistrationRequest = appAuthConfig
|
|
497
602
|
.pick({ self_registration: true, allowed_domains: true, allowed_origins: true })
|
|
498
603
|
.strict();
|
|
499
|
-
/** `PUT /api/apps/:id/auth-config/
|
|
604
|
+
/** `PUT /api/apps/:id/auth-config/sign-in` — how the app's users sign in, and whether they give a second factor. */
|
|
605
|
+
export const putAppAuthSignInRequest = appAuthConfig
|
|
606
|
+
.pick({ sign_in_methods: true, two_factor: true })
|
|
607
|
+
.strict();
|
|
608
|
+
/**
|
|
609
|
+
* `PUT /api/apps/:id/auth-config/urls` — the app's home page and the four
|
|
610
|
+
* pages Fleetless's mails and the MCP sign-in point at. One slice, because
|
|
611
|
+
* the console's Pages section owns all five; each `null` falls back to the
|
|
612
|
+
* hosted page.
|
|
613
|
+
*/
|
|
500
614
|
export const putAppAuthUrlsRequest = appAuthConfig
|
|
501
|
-
.pick({ invite_url: true, verify_url: true, reset_url: true })
|
|
615
|
+
.pick({ app_url: true, invite_url: true, verify_url: true, reset_url: true, mcp_login_url: true })
|
|
502
616
|
.strict();
|
|
503
617
|
/**
|
|
504
|
-
* `PUT /api/apps/:id/auth-config/mcp` — the switch
|
|
505
|
-
*
|
|
506
|
-
*
|
|
618
|
+
* `PUT /api/apps/:id/auth-config/mcp` — the switch alone. Its login URL moved
|
|
619
|
+
* to the `urls` slice: on without a URL no longer refuses anything, because
|
|
620
|
+
* the hosted MCP sign-in stands in for it.
|
|
507
621
|
*/
|
|
508
622
|
export const putAppAuthMcpRequest = appAuthConfig
|
|
509
|
-
.pick({ mcp_enabled: true
|
|
623
|
+
.pick({ mcp_enabled: true })
|
|
624
|
+
.strict();
|
|
625
|
+
/** `PUT /api/apps/:id/auth-config/look` — the hosted pages' accent colour. The logo is its own write, a raw image body. */
|
|
626
|
+
export const putAppAuthLookRequest = appAuthConfig
|
|
627
|
+
.pick({ hosted_accent: true })
|
|
510
628
|
.strict();
|
|
511
629
|
/**
|
|
512
|
-
* The
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
630
|
+
* The four mails a developer may replace with their own template: the
|
|
631
|
+
* invitation, the address confirmation, the password reset and the sign-in
|
|
632
|
+
* code. Mails to *Fleetless* users — a team invitation, a console sign-in
|
|
633
|
+
* code — stay Fleetless default and are deliberately not customisable: they
|
|
634
|
+
* are about this platform, not about the developer's product.
|
|
516
635
|
*/
|
|
517
|
-
export const mailTemplateKind = z.enum(['invite', 'verify', 'reset']);
|
|
636
|
+
export const mailTemplateKind = z.enum(['invite', 'verify', 'reset', 'login_code']);
|
|
518
637
|
/**
|
|
519
638
|
* **Every variable a template may name, and the list is closed.**
|
|
520
639
|
*
|
|
@@ -532,9 +651,11 @@ export const MAIL_TEMPLATE_VARIABLES = [
|
|
|
532
651
|
'role.name',
|
|
533
652
|
'link',
|
|
534
653
|
'expires_in_hours',
|
|
654
|
+
'code',
|
|
655
|
+
'expires_in_minutes',
|
|
535
656
|
];
|
|
536
657
|
/**
|
|
537
|
-
* **The Fleetless default text for the
|
|
658
|
+
* **The Fleetless default text for the four app mails.**
|
|
538
659
|
*
|
|
539
660
|
* It lives here rather than in the cloud because two products send the same
|
|
540
661
|
* words: the cloud renders these when an app has no template of its own, and
|
|
@@ -568,12 +689,15 @@ export const MAIL_TEMPLATE_VARIABLES = [
|
|
|
568
689
|
* that one is written by an authenticated developer about somebody they
|
|
569
690
|
* invited.
|
|
570
691
|
*
|
|
571
|
-
* **`expires_in_hours` is the
|
|
692
|
+
* **`expires_in_hours` is the lifetime variable of the three link mails**, and
|
|
572
693
|
* the three values are 1, 24 and 168. "The next 168 hours" is not how a person
|
|
573
694
|
* says a week, so each default converts: 48 and up reads in days, exactly one
|
|
574
695
|
* reads "1 hour", everything else reads in hours. The conversion is in the
|
|
575
696
|
* template rather than in a new variable because a custom template has the
|
|
576
|
-
* same problem and this is the spelling it can copy.
|
|
697
|
+
* same problem and this is the spelling it can copy. The sign-in code mail
|
|
698
|
+
* carries no link: it names the `code` and its lifetime in
|
|
699
|
+
* `expires_in_minutes` (10), and greets nobody, since whoever asked for it
|
|
700
|
+
* typed the address unauthenticated.
|
|
577
701
|
*
|
|
578
702
|
* **What contracts does NOT assert about these.** That they compile as Liquid
|
|
579
703
|
* is the cloud's business — contracts has no renderer and adding one to check
|
|
@@ -588,7 +712,7 @@ export const DEFAULT_MAIL_TEMPLATES = {
|
|
|
588
712
|
|
|
589
713
|
{{ org.name }} has invited you to {{ app.name }} as {{ role.name }}.
|
|
590
714
|
|
|
591
|
-
Accept the invitation
|
|
715
|
+
Accept the invitation:
|
|
592
716
|
{{ link }}
|
|
593
717
|
|
|
594
718
|
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 +743,13 @@ The link works for the next {% if expires_in_hours >= 48 %}{{ expires_in_hours |
|
|
|
619
743
|
`,
|
|
620
744
|
html: null,
|
|
621
745
|
},
|
|
746
|
+
login_code: {
|
|
747
|
+
subject: 'Your {{ app.name }} sign-in code',
|
|
748
|
+
text: `Your sign-in code for {{ app.name }} is {{ code }}.
|
|
749
|
+
|
|
750
|
+
It works once, for {{ expires_in_minutes }} minutes. If you did not ask for it, ignore this mail.`,
|
|
751
|
+
html: null,
|
|
752
|
+
},
|
|
622
753
|
};
|
|
623
754
|
/**
|
|
624
755
|
* One stored template. `html` is nullable because the mailer's HTML part is
|
|
@@ -626,7 +757,7 @@ The link works for the next {% if expires_in_hours >= 48 %}{{ expires_in_hours |
|
|
|
626
757
|
* should not have to write the same words twice.
|
|
627
758
|
*/
|
|
628
759
|
export const appMailTemplate = z.object({
|
|
629
|
-
kind: mailTemplateKind.meta({ description: 'Which of the
|
|
760
|
+
kind: mailTemplateKind.meta({ description: 'Which of the four mails this template replaces.' }),
|
|
630
761
|
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
762
|
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
763
|
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 +765,7 @@ export const appMailTemplate = z.object({
|
|
|
634
765
|
});
|
|
635
766
|
/** `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
767
|
export const appMailTemplateListResponse = z.object({
|
|
637
|
-
templates: z.array(appMailTemplate).max(
|
|
768
|
+
templates: z.array(appMailTemplate).max(4).meta({
|
|
638
769
|
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
770
|
}),
|
|
640
771
|
});
|
package/dist/apps.d.ts
CHANGED
|
@@ -151,9 +151,8 @@ export declare const createServerKeyResponse: z.ZodObject<{
|
|
|
151
151
|
export type CreateServerKeyResponse = z.infer<typeof createServerKeyResponse>;
|
|
152
152
|
/**
|
|
153
153
|
* Every app starts with `observe` and `operate`; custom roles are allowed
|
|
154
|
-
* too. `builtin` marks the two starting roles — editable
|
|
155
|
-
*
|
|
156
|
-
* from.
|
|
154
|
+
* too. `builtin` marks the two starting roles — editable like any other,
|
|
155
|
+
* the flag only tells the console where they came from.
|
|
157
156
|
*/
|
|
158
157
|
export declare const role: z.ZodObject<{
|
|
159
158
|
id: z.ZodUUID;
|
|
@@ -172,41 +171,6 @@ export declare const roleListResponse: z.ZodObject<{
|
|
|
172
171
|
}, z.core.$strip>>;
|
|
173
172
|
}, z.core.$strip>;
|
|
174
173
|
export type RoleListResponse = z.infer<typeof roleListResponse>;
|
|
175
|
-
/**
|
|
176
|
-
* The body of `PATCH /api/apps/:id/roles/:roleId`: the role's new name.
|
|
177
|
-
*
|
|
178
|
-
* The same bounds as `role.name`, trimmed. Names are unique per app, compared
|
|
179
|
-
* exactly as stored after trimming; a clash answers `409 role_name_taken`.
|
|
180
|
-
*/
|
|
181
|
-
export declare const roleRenameRequest: z.ZodObject<{
|
|
182
|
-
name: z.ZodString;
|
|
183
|
-
}, z.core.$strict>;
|
|
184
|
-
export type RoleRenameRequest = z.infer<typeof roleRenameRequest>;
|
|
185
|
-
/**
|
|
186
|
-
* The query of `DELETE /api/apps/:id/roles/:roleId`.
|
|
187
|
-
*
|
|
188
|
-
* `move_to` is what makes a held role deletable: every app user and pending
|
|
189
|
-
* invitation holding the role moves to it, and so does the app's default when
|
|
190
|
-
* it pointed at the role, in the same transaction as the delete. Without it a
|
|
191
|
-
* held role answers `409 role_in_use` with `roleInUseDetails`, so a client
|
|
192
|
-
* can ask where they should go instead of guessing.
|
|
193
|
-
*/
|
|
194
|
-
export declare const roleDeleteQuery: z.ZodObject<{
|
|
195
|
-
move_to: z.ZodOptional<z.ZodUUID>;
|
|
196
|
-
}, z.core.$strict>;
|
|
197
|
-
export type RoleDeleteQuery = z.infer<typeof roleDeleteQuery>;
|
|
198
|
-
/**
|
|
199
|
-
* `details` of `409 role_in_use`: what still holds the role.
|
|
200
|
-
*
|
|
201
|
-
* All three are reported, zeros included, so a client renders one sentence
|
|
202
|
-
* from one shape rather than inferring a missing key.
|
|
203
|
-
*/
|
|
204
|
-
export declare const roleInUseDetails: z.ZodObject<{
|
|
205
|
-
users: z.ZodNumber;
|
|
206
|
-
invitations: z.ZodNumber;
|
|
207
|
-
is_default: z.ZodBoolean;
|
|
208
|
-
}, z.core.$strict>;
|
|
209
|
-
export type RoleInUseDetails = z.infer<typeof roleInUseDetails>;
|
|
210
174
|
/**
|
|
211
175
|
* The rights matrix of one role: which slugs of which robot it may use, plus
|
|
212
176
|
* the capabilities roles also govern. `capabilities`' own doc comment
|
package/dist/apps.js
CHANGED
|
@@ -206,9 +206,8 @@ export const createServerKeyResponse = z.object({
|
|
|
206
206
|
});
|
|
207
207
|
/**
|
|
208
208
|
* Every app starts with `observe` and `operate`; custom roles are allowed
|
|
209
|
-
* too. `builtin` marks the two starting roles — editable
|
|
210
|
-
*
|
|
211
|
-
* from.
|
|
209
|
+
* too. `builtin` marks the two starting roles — editable like any other,
|
|
210
|
+
* the flag only tells the console where they came from.
|
|
212
211
|
*/
|
|
213
212
|
export const role = z.object({
|
|
214
213
|
id: z.uuid().meta({
|
|
@@ -221,7 +220,7 @@ export const role = z.object({
|
|
|
221
220
|
description: 'The role\'s name, shown wherever a user\'s access is chosen. The two roles every app starts with are named `observe` and `operate`.',
|
|
222
221
|
}),
|
|
223
222
|
builtin: z.boolean().meta({
|
|
224
|
-
description: '`true` for the two roles every app starts with. Their **rights may be re-scoped** exactly like a custom role\'s, through `PUT /api/apps/:id/roles/:roleId/permissions` — the flag exists so the console can explain where they came from, not to protect them.
|
|
223
|
+
description: '`true` for the two roles every app starts with. Their **rights may be re-scoped** exactly like a custom role\'s, through `PUT /api/apps/:id/roles/:roleId/permissions` — the flag exists so the console can explain where they came from, not to protect them. It does not make them renamable or deletable — no route does that for any role.',
|
|
225
224
|
}),
|
|
226
225
|
});
|
|
227
226
|
/** What `GET /api/apps/:id/roles` answers: the app's roles, builtin and custom alike. */
|
|
@@ -230,48 +229,6 @@ export const roleListResponse = z.object({
|
|
|
230
229
|
description: 'The app\'s roles, built-in and custom alike, ordered by `created_at` and then by `name`. The tie-break is not cosmetic — the two built-in roles are inserted in one statement and share a creation time to the microsecond, so never read a role by position.',
|
|
231
230
|
}),
|
|
232
231
|
});
|
|
233
|
-
/**
|
|
234
|
-
* The body of `PATCH /api/apps/:id/roles/:roleId`: the role's new name.
|
|
235
|
-
*
|
|
236
|
-
* The same bounds as `role.name`, trimmed. Names are unique per app, compared
|
|
237
|
-
* exactly as stored after trimming; a clash answers `409 role_name_taken`.
|
|
238
|
-
*/
|
|
239
|
-
export const roleRenameRequest = z.object({
|
|
240
|
-
name: z.string().trim().min(1).max(60).meta({
|
|
241
|
-
description: 'The new name, trimmed, 1 to 60 characters. Unique per app: another role of this app with the same name answers `409 role_name_taken`. The role\'s users keep it under its new name.',
|
|
242
|
-
}),
|
|
243
|
-
}).strict();
|
|
244
|
-
/**
|
|
245
|
-
* The query of `DELETE /api/apps/:id/roles/:roleId`.
|
|
246
|
-
*
|
|
247
|
-
* `move_to` is what makes a held role deletable: every app user and pending
|
|
248
|
-
* invitation holding the role moves to it, and so does the app's default when
|
|
249
|
-
* it pointed at the role, in the same transaction as the delete. Without it a
|
|
250
|
-
* held role answers `409 role_in_use` with `roleInUseDetails`, so a client
|
|
251
|
-
* can ask where they should go instead of guessing.
|
|
252
|
-
*/
|
|
253
|
-
export const roleDeleteQuery = z.object({
|
|
254
|
-
move_to: z.uuid().optional().meta({
|
|
255
|
-
description: 'Another role of the same app that takes over the deleted role\'s app users, pending invitations and, when it applies, the app\'s default. The role itself or a role of another app answers `400 validation_error`.',
|
|
256
|
-
}),
|
|
257
|
-
}).strict();
|
|
258
|
-
/**
|
|
259
|
-
* `details` of `409 role_in_use`: what still holds the role.
|
|
260
|
-
*
|
|
261
|
-
* All three are reported, zeros included, so a client renders one sentence
|
|
262
|
-
* from one shape rather than inferring a missing key.
|
|
263
|
-
*/
|
|
264
|
-
export const roleInUseDetails = z.object({
|
|
265
|
-
users: z.number().int().nonnegative().meta({
|
|
266
|
-
description: 'App users whose role this is.',
|
|
267
|
-
}),
|
|
268
|
-
invitations: z.number().int().nonnegative().meta({
|
|
269
|
-
description: 'Pending invitations that would grant this role when accepted.',
|
|
270
|
-
}),
|
|
271
|
-
is_default: z.boolean().meta({
|
|
272
|
-
description: '`true` when this is the app\'s `default_role_id`; the default then moves with the users to `move_to`.',
|
|
273
|
-
}),
|
|
274
|
-
}).strict();
|
|
275
232
|
/**
|
|
276
233
|
* The rights matrix of one role: which slugs of which robot it may use, plus
|
|
277
234
|
* the capabilities roles also govern. `capabilities`' own doc comment
|
package/dist/audit.d.ts
CHANGED
|
@@ -72,7 +72,6 @@ export declare const auditQuery: z.ZodObject<{
|
|
|
72
72
|
action_prefix: z.ZodOptional<z.ZodString>;
|
|
73
73
|
actor_id: z.ZodOptional<z.ZodUUID>;
|
|
74
74
|
target_kind: z.ZodOptional<z.ZodString>;
|
|
75
|
-
target_id: z.ZodOptional<z.ZodString>;
|
|
76
75
|
from_ms: z.ZodOptional<z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<number, string | number>>, z.ZodNumber>>;
|
|
77
76
|
to_ms: z.ZodOptional<z.ZodPipe<z.ZodPipe<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>, z.ZodTransform<number, string | number>>, z.ZodNumber>>;
|
|
78
77
|
}, z.core.$strict>;
|
package/dist/audit.js
CHANGED
|
@@ -155,19 +155,6 @@ export const auditQuery = z.object({
|
|
|
155
155
|
actor_id: z.uuid().optional(),
|
|
156
156
|
/** Only events about this kind of target, e.g. `robot`. */
|
|
157
157
|
target_kind: z.string().min(1).max(40).optional(),
|
|
158
|
-
/**
|
|
159
|
-
* Only events about this target — and, for a robot, also the events that
|
|
160
|
-
* name it in `details.robot_id`, so a robot's log includes what was
|
|
161
|
-
* started on it.
|
|
162
|
-
*
|
|
163
|
-
* **A string, not `z.uuid()`**, unlike `actor_id` above: `target.id` is a
|
|
164
|
-
* string in this contract and text in the cloud's table, so a uuid rule
|
|
165
|
-
* here would refuse ids the log can hold, and no value can fail a cast.
|
|
166
|
-
*/
|
|
167
|
-
target_id: z.string().min(1).max(200).optional().meta({
|
|
168
|
-
description: 'Events whose target is this id; for a robot also the events that name it in `details.robot_id` (`action.invoked`, ' +
|
|
169
|
-
'`service.called`, …), so a robot\'s events include what was started on it.',
|
|
170
|
-
}),
|
|
171
158
|
/**
|
|
172
159
|
* Absolute bounds in unix milliseconds, **half-open `[from, to)`** — the
|
|
173
160
|
* same rule the history shapes follow.
|