@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.
- package/CHANGELOG.md +96 -33
- package/artifacts/openapi.json +2049 -773
- package/artifacts/routes.json +1665 -314
- 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/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 -34
- package/dist/app-users.js +177 -45
- 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 +34 -34
- package/dist/identity.d.ts +172 -123
- package/dist/identity.js +189 -101
- 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 +19 -0
- package/dist/routes.js +499 -223
- 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,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/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.
|