@faable/faable 4.20.0 → 4.21.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/dist/api/FaableApi.js +11 -3
- package/dist/commands/auth/clients/login_methods.js +83 -0
- package/dist/commands/auth/clients/update.js +69 -3
- package/dist/commands/deploy/secrets/after_save.js +67 -0
- package/dist/commands/deploy/secrets/rm.js +16 -3
- package/dist/commands/deploy/secrets/set.js +14 -6
- package/dist/mcp/tools.js +59 -1
- package/package.json +1 -1
package/dist/api/FaableApi.js
CHANGED
|
@@ -272,13 +272,21 @@ class FaableApi {
|
|
|
272
272
|
})));
|
|
273
273
|
}
|
|
274
274
|
// Replace the app's whole secret set (the endpoint deletes and recreates).
|
|
275
|
-
//
|
|
276
|
-
// app; the per-secret upsert/delete endpoints are not used by the CLI.
|
|
275
|
+
// The per-secret upsert/delete endpoints are not used by the CLI.
|
|
277
276
|
// The endpoint stamps the created secrets with the team from the request
|
|
278
277
|
// context, which a CLI user token does not carry — pass the app's team
|
|
279
278
|
// (from getApp) so it travels as the `x-faable-project` header.
|
|
279
|
+
//
|
|
280
|
+
// `with_redeploy=true` (api §403): a running app restarts to apply the new
|
|
281
|
+
// values, but an app with nothing running whose last deployment failed at
|
|
282
|
+
// runtime has nothing to restart — the api relaunches that deployment and
|
|
283
|
+
// says so in `redeploy`. An api that predates it ignores the parameter and
|
|
284
|
+
// answers the bare array: read that as "no redeploy to report".
|
|
280
285
|
async createSecretsBatch(context_id, team, secrets) {
|
|
281
|
-
|
|
286
|
+
const body = await data(this.client.post(`/secret/createbatch`, { context_id, secrets }, { headers: projectHeader(team), params: { with_redeploy: true } }));
|
|
287
|
+
if (Array.isArray(body))
|
|
288
|
+
return { secrets: body, redeploy: null };
|
|
289
|
+
return { secrets: body?.secrets ?? [], redeploy: body?.redeploy ?? null };
|
|
282
290
|
}
|
|
283
291
|
async updateApp(app_id, params) {
|
|
284
292
|
return data(this.client.post(`/app/${app_id}`, params));
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// A client's own `login_methods` overrides the tenant's FIELD BY FIELD
|
|
2
|
+
// (auth: resolveLoginMethodsConfig): a field the client does not set is
|
|
3
|
+
// inherited. The update route stores the object whole, so every change here is
|
|
4
|
+
// a read-modify-write of what the client has now, and a field this command was
|
|
5
|
+
// not asked about keeps whatever it was — its own value or the inheritance.
|
|
6
|
+
const has_login_method_changes = (c) => !!(c.inherit ||
|
|
7
|
+
c.passkey ||
|
|
8
|
+
c.only?.length ||
|
|
9
|
+
c.disable?.length ||
|
|
10
|
+
c.enable?.length);
|
|
11
|
+
/**
|
|
12
|
+
* A connection by id or by name (case-insensitive). A name two connections
|
|
13
|
+
* share is refused rather than guessed: removing the wrong login method is
|
|
14
|
+
* not something to find out on the login screen.
|
|
15
|
+
*/
|
|
16
|
+
const resolve_connection = (connections, ref) => {
|
|
17
|
+
const by_id = connections.find(c => c.id === ref);
|
|
18
|
+
if (by_id)
|
|
19
|
+
return by_id;
|
|
20
|
+
const by_name = connections.filter(c => c.connection_name?.toLowerCase() === ref.toLowerCase());
|
|
21
|
+
if (by_name.length === 1)
|
|
22
|
+
return by_name[0];
|
|
23
|
+
const known = connections
|
|
24
|
+
.map(c => `${c.connection_name ?? '?'} (${c.id})`)
|
|
25
|
+
.join(', ');
|
|
26
|
+
if (by_name.length > 1) {
|
|
27
|
+
throw new Error(`"${ref}" names ${by_name.length} connections — use the id: ${by_name.map(c => c.id).join(', ')}`);
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Unknown connection "${ref}". This tenant has: ${known}`);
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* The client's next `login_methods`, or `null` to inherit everything from the
|
|
33
|
+
* tenant. `undefined` when nothing was asked for.
|
|
34
|
+
*/
|
|
35
|
+
const apply_login_method_changes = (own, tenant, connections, changes) => {
|
|
36
|
+
if (!has_login_method_changes(changes))
|
|
37
|
+
return undefined;
|
|
38
|
+
if (changes.inherit)
|
|
39
|
+
return null;
|
|
40
|
+
const next = { ...(own ?? {}) };
|
|
41
|
+
if (changes.only?.length || changes.disable?.length || changes.enable?.length) {
|
|
42
|
+
// Starts from what the client shows TODAY: its own list, or the tenant's
|
|
43
|
+
// it inherits — otherwise the first own list would silently re-offer
|
|
44
|
+
// everything the tenant had removed.
|
|
45
|
+
const disabled = new Set(own?.disabled_connections ?? tenant?.disabled_connections ?? []);
|
|
46
|
+
if (changes.only?.length) {
|
|
47
|
+
const keep = new Set(changes.only.map(ref => resolve_connection(connections, ref).id));
|
|
48
|
+
disabled.clear();
|
|
49
|
+
for (const c of connections)
|
|
50
|
+
if (!keep.has(c.id))
|
|
51
|
+
disabled.add(c.id);
|
|
52
|
+
}
|
|
53
|
+
for (const ref of changes.disable ?? []) {
|
|
54
|
+
disabled.add(resolve_connection(connections, ref).id);
|
|
55
|
+
}
|
|
56
|
+
for (const ref of changes.enable ?? []) {
|
|
57
|
+
disabled.delete(resolve_connection(connections, ref).id);
|
|
58
|
+
}
|
|
59
|
+
next.disabled_connections = [...disabled];
|
|
60
|
+
}
|
|
61
|
+
if (changes.passkey === 'on')
|
|
62
|
+
next.passkey_login_enabled = true;
|
|
63
|
+
else if (changes.passkey === 'off')
|
|
64
|
+
next.passkey_login_enabled = false;
|
|
65
|
+
else if (changes.passkey === 'inherit')
|
|
66
|
+
delete next.passkey_login_enabled;
|
|
67
|
+
return Object.keys(next).length ? next : null;
|
|
68
|
+
};
|
|
69
|
+
/** What the login screen of this client offers, for the summary line. */
|
|
70
|
+
const effective_login_methods = (own, tenant, connections) => {
|
|
71
|
+
const disabled = new Set(own?.disabled_connections ?? tenant?.disabled_connections ?? []);
|
|
72
|
+
const order = new Set(own?.order ?? tenant?.order ?? []);
|
|
73
|
+
const passkey = own?.passkey_login_enabled ?? tenant?.passkey_login_enabled ?? false;
|
|
74
|
+
return {
|
|
75
|
+
passkey,
|
|
76
|
+
// OIDC providers stay off the screen unless `order` names them.
|
|
77
|
+
connections: connections.filter(c => c.enabled !== false &&
|
|
78
|
+
!disabled.has(c.id) &&
|
|
79
|
+
(c.connection_type !== 'oidc' || order.has(c.id)))
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export { apply_login_method_changes, effective_login_methods, has_login_method_changes, resolve_connection };
|
|
@@ -3,6 +3,7 @@ import { log } from '../../../log.js';
|
|
|
3
3
|
import { tenant_options } from '../options.js';
|
|
4
4
|
import { json_option, print_json } from '../../../lib/listing.js';
|
|
5
5
|
import { parse_field_spec, parse_fields_json, apply_field_changes, format_field } from './profile_fields.js';
|
|
6
|
+
import { has_login_method_changes, apply_login_method_changes, effective_login_methods } from './login_methods.js';
|
|
6
7
|
import { resolve_client } from './resolve.js';
|
|
7
8
|
|
|
8
9
|
// The update route replaces each list whole, so every list change here is a
|
|
@@ -19,7 +20,7 @@ const edit_list = (current, add = [], remove = []) => {
|
|
|
19
20
|
};
|
|
20
21
|
const clients_update = {
|
|
21
22
|
command: 'update <client_id>',
|
|
22
|
-
describe: 'Update an OAuth client (name, URLs, profile fields)',
|
|
23
|
+
describe: 'Update an OAuth client (name, URLs, profile fields, login methods)',
|
|
23
24
|
builder: yargs => json_option(tenant_options(yargs))
|
|
24
25
|
.positional('client_id', {
|
|
25
26
|
type: 'string',
|
|
@@ -83,9 +84,40 @@ const clients_update = {
|
|
|
83
84
|
.option('inherit-fields', {
|
|
84
85
|
type: 'boolean',
|
|
85
86
|
description: "Drop this client's own profile field list and use the tenant's again"
|
|
87
|
+
})
|
|
88
|
+
.option('only-connections', {
|
|
89
|
+
type: 'string',
|
|
90
|
+
array: true,
|
|
91
|
+
description: 'Offer exactly these connections (id or name) on this client\'s login screen; every other one is removed'
|
|
92
|
+
})
|
|
93
|
+
.option('disable-connection', {
|
|
94
|
+
type: 'string',
|
|
95
|
+
array: true,
|
|
96
|
+
description: 'Stop offering a connection (id or name) on this client, also at /authorize — repeatable'
|
|
97
|
+
})
|
|
98
|
+
.option('enable-connection', {
|
|
99
|
+
type: 'string',
|
|
100
|
+
array: true,
|
|
101
|
+
description: 'Offer a removed connection again — repeatable'
|
|
102
|
+
})
|
|
103
|
+
.option('passkey-login', {
|
|
104
|
+
choices: ['on', 'off', 'inherit'],
|
|
105
|
+
description: "Passkey sign-in button on this client's login screen; inherit follows the tenant (Pro plan)"
|
|
106
|
+
})
|
|
107
|
+
.option('inherit-login-methods', {
|
|
108
|
+
type: 'boolean',
|
|
109
|
+
description: "Drop this client's own login methods and follow the tenant's again"
|
|
86
110
|
})
|
|
87
111
|
.conflicts('fields-json', ['add-field', 'remove-field', 'inherit-fields'])
|
|
88
112
|
.conflicts('inherit-fields', ['add-field', 'remove-field'])
|
|
113
|
+
.conflicts('inherit-login-methods', [
|
|
114
|
+
'only-connections',
|
|
115
|
+
'disable-connection',
|
|
116
|
+
'enable-connection',
|
|
117
|
+
'passkey-login'
|
|
118
|
+
])
|
|
119
|
+
.conflicts('only-connections', 'disable-connection')
|
|
120
|
+
.example('$0 auth clients update my-admin --only-connections OTP --passkey-login on', 'Sign in to my-admin only with a passkey or an email code')
|
|
89
121
|
.example('$0 auth clients update my-app --add-field profile:enum:student,teacher', 'Let users of my-app pick their profile with auth-js updateProfile')
|
|
90
122
|
.example('$0 auth clients update my-app --remove-field school', 'Stop accepting a field')
|
|
91
123
|
.example('$0 auth clients update my-app --add-callback https://app.example.com/cb', 'Allow one more redirect URL, keeping the others')
|
|
@@ -110,6 +142,28 @@ const clients_update = {
|
|
|
110
142
|
const callbacks = edit_list(client.callbacks, args.addCallback, args.removeCallback);
|
|
111
143
|
const logout_urls = edit_list(client.logout_urls, args.addLogoutUrl, args.removeLogoutUrl);
|
|
112
144
|
const web_origins = edit_list(client.web_origins, args.addWebOrigin, args.removeWebOrigin);
|
|
145
|
+
const login_changes = {
|
|
146
|
+
only: args.onlyConnections,
|
|
147
|
+
disable: args.disableConnection,
|
|
148
|
+
enable: args.enableConnection,
|
|
149
|
+
passkey: args.passkeyLogin,
|
|
150
|
+
inherit: args.inheritLoginMethods
|
|
151
|
+
};
|
|
152
|
+
const own_login = client
|
|
153
|
+
.login_methods;
|
|
154
|
+
let tenant_login;
|
|
155
|
+
let connections = [];
|
|
156
|
+
let login_methods;
|
|
157
|
+
if (has_login_method_changes(login_changes)) {
|
|
158
|
+
const [account, all] = await Promise.all([
|
|
159
|
+
api.currentAccount(),
|
|
160
|
+
api.connectionList().all()
|
|
161
|
+
]);
|
|
162
|
+
tenant_login = account
|
|
163
|
+
.login_methods;
|
|
164
|
+
connections = all;
|
|
165
|
+
login_methods = apply_login_method_changes(own_login, tenant_login, connections, login_changes);
|
|
166
|
+
}
|
|
113
167
|
const patch = {
|
|
114
168
|
...(args.name !== undefined ? { name: args.name } : {}),
|
|
115
169
|
...(args.description !== undefined
|
|
@@ -118,15 +172,27 @@ const clients_update = {
|
|
|
118
172
|
...(callbacks ? { callbacks } : {}),
|
|
119
173
|
...(logout_urls ? { logout_urls } : {}),
|
|
120
174
|
...(web_origins ? { web_origins } : {}),
|
|
121
|
-
...(profile_fields !== undefined ? { profile_fields } : {})
|
|
175
|
+
...(profile_fields !== undefined ? { profile_fields } : {}),
|
|
176
|
+
...(login_methods !== undefined ? { login_methods } : {})
|
|
122
177
|
};
|
|
123
178
|
if (Object.keys(patch).length === 0) {
|
|
124
|
-
throw new Error('Nothing to update — pass a --name, --description, an --add-*/--remove-* option, --fields-json
|
|
179
|
+
throw new Error('Nothing to update — pass a --name, --description, an --add-*/--remove-* option, --fields-json, --inherit-fields or a login-method option');
|
|
125
180
|
}
|
|
126
181
|
const updated = await api.clientUpdate(client.id, patch);
|
|
127
182
|
if (args.json)
|
|
128
183
|
return print_json(updated);
|
|
129
184
|
log.info(`✅ Updated client "${updated.name}" (${updated.client_id})`);
|
|
185
|
+
if (login_methods !== undefined) {
|
|
186
|
+
const now = effective_login_methods(updated
|
|
187
|
+
.login_methods, tenant_login, connections);
|
|
188
|
+
const names = now.connections.map(c => `${c.connection_name} (${c.connection_type})`);
|
|
189
|
+
if (now.passkey)
|
|
190
|
+
names.unshift('passkey');
|
|
191
|
+
log.info(` Login screen: ${names.length ? names.join(', ') : 'nothing'}`);
|
|
192
|
+
if (login_methods === null) {
|
|
193
|
+
log.info(" Login methods: inherited from the tenant");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
130
196
|
if (profile_fields !== undefined) {
|
|
131
197
|
const now = updated.profile_fields;
|
|
132
198
|
if (!now) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// What happens to the app after a secrets save, in the words the user reads.
|
|
2
|
+
//
|
|
3
|
+
// A running app restarts to apply the new values. An app with NOTHING running
|
|
4
|
+
// whose last deployment failed at runtime (a crashloop over a missing
|
|
5
|
+
// variable, the usual reason to come and set one) has nothing to restart: the
|
|
6
|
+
// api relaunches that deployment itself and reports it (api §403). Promising
|
|
7
|
+
// a restart there was wrong — nothing was coming, or a whole new deployment
|
|
8
|
+
// was.
|
|
9
|
+
//
|
|
10
|
+
// Every command carries `-a <app id>`: these lines get copied, and outside
|
|
11
|
+
// the app's checkout a bare command resolves no app.
|
|
12
|
+
const after_save_lines = (args) => {
|
|
13
|
+
const { app, redeploy, changed } = args;
|
|
14
|
+
const what = args.what ?? 'the new variables';
|
|
15
|
+
const a = `-a ${app.id}`;
|
|
16
|
+
if (!redeploy) {
|
|
17
|
+
return [
|
|
18
|
+
{
|
|
19
|
+
level: 'info',
|
|
20
|
+
text: changed
|
|
21
|
+
? `ℹ️ The app is restarting to apply the changes.`
|
|
22
|
+
: `ℹ️ Already up to date — the app was not restarted.`
|
|
23
|
+
}
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
const failed = redeploy.failed_deployment_id;
|
|
27
|
+
if (redeploy.status === 'started') {
|
|
28
|
+
const id = redeploy.deployment_id;
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
level: 'info',
|
|
32
|
+
text: `🔁 Redeploying ${app.name} with ${what} (deployment ${id}): its last deployment ${failed} had failed and nothing was running.`
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
level: 'info',
|
|
36
|
+
text: `Follow it with: faable deploy logs --build -d ${id} ${a} --follow · status: faable deploy status ${a}`
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
if (redeploy.status === 'needs_rebuild') {
|
|
41
|
+
return [
|
|
42
|
+
{
|
|
43
|
+
level: 'warn',
|
|
44
|
+
text: `⚠️ Nothing is running: the last deployment ${failed} failed to build, so ${what} are saved but not deployed.`
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
level: 'warn',
|
|
48
|
+
text: `Rebuild it with: faable deploy redeploy ${failed} ${a} · why it failed: faable deploy logs --build -d ${failed} ${a}`
|
|
49
|
+
}
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
return [
|
|
53
|
+
{
|
|
54
|
+
level: 'warn',
|
|
55
|
+
text: `⚠️ Nothing is running and Faable could not relaunch the failed deployment ${failed}, so ${what} are saved but not deployed.`
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
level: 'warn',
|
|
59
|
+
text: `Relaunch it by hand (it tells you why if it is refused): faable deploy redeploy ${failed} ${a}`
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
};
|
|
63
|
+
// The `restarting` of the --json output: only a running app restarts. When
|
|
64
|
+
// the api reports a redeploy (started or not), nothing restarts.
|
|
65
|
+
const is_restarting = (redeploy, changed) => changed && !redeploy;
|
|
66
|
+
|
|
67
|
+
export { after_save_lines, is_restarting };
|
|
@@ -2,6 +2,7 @@ import { requireApi } from '../../../api/context.js';
|
|
|
2
2
|
import { print_json, json_option } from '../../../lib/listing.js';
|
|
3
3
|
import { log } from '../../../log.js';
|
|
4
4
|
import { resolve_app_id } from '../resolve_app_id.js';
|
|
5
|
+
import { is_restarting, after_save_lines } from './after_save.js';
|
|
5
6
|
import { remove_app_secret } from './merge.js';
|
|
6
7
|
import { confirm } from '../../../lib/interactive.js';
|
|
7
8
|
|
|
@@ -44,12 +45,24 @@ const secrets_rm = {
|
|
|
44
45
|
log.info('Cancelled.');
|
|
45
46
|
return;
|
|
46
47
|
}
|
|
47
|
-
await ctx.api.createSecretsBatch(app.id, app.team, remaining);
|
|
48
|
+
const { redeploy } = await ctx.api.createSecretsBatch(app.id, app.team, remaining);
|
|
48
49
|
if (args.json) {
|
|
49
|
-
return print_json({
|
|
50
|
+
return print_json({
|
|
51
|
+
deleted: true,
|
|
52
|
+
name: args.name,
|
|
53
|
+
restarting: is_restarting(redeploy, true),
|
|
54
|
+
redeploy
|
|
55
|
+
});
|
|
50
56
|
}
|
|
51
57
|
log.info(`🗑️ Removed secret ${args.name} from ${app_id}.`);
|
|
52
|
-
|
|
58
|
+
const lines = after_save_lines({
|
|
59
|
+
app,
|
|
60
|
+
redeploy,
|
|
61
|
+
changed: true,
|
|
62
|
+
what: 'the updated variables'
|
|
63
|
+
});
|
|
64
|
+
for (const { level, text } of lines)
|
|
65
|
+
log[level](text);
|
|
53
66
|
}
|
|
54
67
|
};
|
|
55
68
|
|
|
@@ -5,6 +5,7 @@ import { print_json, json_option } from '../../../lib/listing.js';
|
|
|
5
5
|
import { read_stdin } from '../../../lib/stdin.js';
|
|
6
6
|
import { log } from '../../../log.js';
|
|
7
7
|
import { resolve_app_id } from '../resolve_app_id.js';
|
|
8
|
+
import { is_restarting, after_save_lines } from './after_save.js';
|
|
8
9
|
import { managed_warning } from './managed_names.js';
|
|
9
10
|
import { merge_app_secrets } from './merge.js';
|
|
10
11
|
import { parse_env } from './parse_env.js';
|
|
@@ -113,12 +114,12 @@ const secrets_set = {
|
|
|
113
114
|
const app = await ctx.api.getApp(app_id);
|
|
114
115
|
const existing = await ctx.api.getAppSecrets(app_id, app.team);
|
|
115
116
|
const merged = merge_app_secrets(existing, parsed);
|
|
116
|
-
await ctx.api.createSecretsBatch(app.id, app.team, merged);
|
|
117
|
+
const { redeploy } = await ctx.api.createSecretsBatch(app.id, app.team, merged);
|
|
117
118
|
const current = new Map(existing
|
|
118
119
|
.filter(s => s.related_model === 'app')
|
|
119
120
|
.map(s => [s.name, s.value]));
|
|
120
121
|
const updated = parsed.filter(({ name }) => current.has(name)).length;
|
|
121
|
-
// Only claim
|
|
122
|
+
// Only claim a restart when something actually changed. Setting a name
|
|
122
123
|
// to the value it already has is a no-op end to end — the API compares
|
|
123
124
|
// before writing and never emits the event, so the pod keeps running —
|
|
124
125
|
// and this line used to promise a restart that was not coming.
|
|
@@ -134,7 +135,10 @@ const secrets_set = {
|
|
|
134
135
|
unchanged: parsed
|
|
135
136
|
.filter(p => current.get(p.name) === p.value)
|
|
136
137
|
.map(p => p.name),
|
|
137
|
-
restarting: changed
|
|
138
|
+
restarting: is_restarting(redeploy, changed),
|
|
139
|
+
// api §403: what the api did for an app with nothing running whose
|
|
140
|
+
// last deployment failed — null when there was nothing to relaunch.
|
|
141
|
+
redeploy
|
|
138
142
|
});
|
|
139
143
|
}
|
|
140
144
|
if (parsed.length <= MAX_DETAILED) {
|
|
@@ -148,9 +152,13 @@ const secrets_set = {
|
|
|
148
152
|
log.info(`🔑 ${parsed.length - updated} added, ${updated} updated`);
|
|
149
153
|
}
|
|
150
154
|
log.info(`✅ ${parsed.length} secret(s) saved to ${app_id}.`);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
for (const { level, text } of after_save_lines({
|
|
156
|
+
app,
|
|
157
|
+
redeploy,
|
|
158
|
+
changed
|
|
159
|
+
})) {
|
|
160
|
+
log[level](text);
|
|
161
|
+
}
|
|
154
162
|
}
|
|
155
163
|
};
|
|
156
164
|
|
package/dist/mcp/tools.js
CHANGED
|
@@ -760,7 +760,7 @@ const TOOLS = [
|
|
|
760
760
|
{
|
|
761
761
|
name: 'set_secrets',
|
|
762
762
|
title: 'Set environment variables',
|
|
763
|
-
description: 'Add or update environment variables of an app.
|
|
763
|
+
description: 'Add or update environment variables of an app. When the app is running, it restarts to apply them (only if a value changed); if its last deployment failed at runtime and nothing is running, Faable redeploys it with the new variables. Returns the names added, updated and unchanged — never the values — and `redeploy`: null when there was nothing to relaunch; otherwise `status` is `started` (follow `deployment_id` with get_deployment), `needs_rebuild` (the last deployment failed to build: retry `failed_deployment_id` with redeploy) or `not_started` (the platform refused the relaunch: retry it with redeploy to see why).',
|
|
764
764
|
input: {
|
|
765
765
|
app,
|
|
766
766
|
project,
|
|
@@ -998,6 +998,64 @@ const TOOLS = [
|
|
|
998
998
|
remove: ['school']
|
|
999
999
|
}
|
|
1000
1000
|
},
|
|
1001
|
+
{
|
|
1002
|
+
name: 'update_auth_client_login',
|
|
1003
|
+
title: "Change how users sign in to an app",
|
|
1004
|
+
description: "Choose which login methods one Auth application offers: which connections (by id or name, as list_auth_connections shows them) and whether the passkey button appears. `only` keeps exactly those connections and removes the rest; `disable`/`enable` change one at a time. A removed connection is refused at /authorize too, not just hidden. `passkey` needs the Pro plan. `inherit: true` drops the app's own settings and follows the tenant's again. Settings not mentioned keep their current value.",
|
|
1005
|
+
input: {
|
|
1006
|
+
client: z
|
|
1007
|
+
.string()
|
|
1008
|
+
.describe('The application: client resource id (client_…) or OAuth client_id'),
|
|
1009
|
+
only: z
|
|
1010
|
+
.array(z.string())
|
|
1011
|
+
.max(50)
|
|
1012
|
+
.optional()
|
|
1013
|
+
.describe('Offer exactly these connections (id or name)'),
|
|
1014
|
+
disable: z
|
|
1015
|
+
.array(z.string())
|
|
1016
|
+
.max(50)
|
|
1017
|
+
.optional()
|
|
1018
|
+
.describe('Connections (id or name) to stop offering'),
|
|
1019
|
+
enable: z
|
|
1020
|
+
.array(z.string())
|
|
1021
|
+
.max(50)
|
|
1022
|
+
.optional()
|
|
1023
|
+
.describe('Removed connections (id or name) to offer again'),
|
|
1024
|
+
passkey: z
|
|
1025
|
+
.enum(['on', 'off', 'inherit'])
|
|
1026
|
+
.optional()
|
|
1027
|
+
.describe('Passkey sign-in button; inherit follows the tenant'),
|
|
1028
|
+
inherit: z
|
|
1029
|
+
.boolean()
|
|
1030
|
+
.optional()
|
|
1031
|
+
.describe("Drop the app's own login methods and use the tenant's"),
|
|
1032
|
+
tenant,
|
|
1033
|
+
project
|
|
1034
|
+
},
|
|
1035
|
+
command: ['auth', 'clients', 'update'],
|
|
1036
|
+
flags: a => [
|
|
1037
|
+
a.client,
|
|
1038
|
+
...(a.only ?? []).flatMap((c) => ['--only-connections', c]),
|
|
1039
|
+
...(a.disable ?? []).flatMap((c) => ['--disable-connection', c]),
|
|
1040
|
+
...(a.enable ?? []).flatMap((c) => ['--enable-connection', c]),
|
|
1041
|
+
...opt('--passkey-login', a.passkey),
|
|
1042
|
+
...(a.inherit ? ['--inherit-login-methods'] : []),
|
|
1043
|
+
...auth_scope(a)
|
|
1044
|
+
],
|
|
1045
|
+
shape: (c) => ({
|
|
1046
|
+
id: c.id,
|
|
1047
|
+
client_id: c.client_id,
|
|
1048
|
+
name: c.name,
|
|
1049
|
+
login_methods: c.login_methods ?? null
|
|
1050
|
+
}),
|
|
1051
|
+
write: true,
|
|
1052
|
+
annotations: AUTH_WRITE,
|
|
1053
|
+
example: {
|
|
1054
|
+
client: 'client_1',
|
|
1055
|
+
only: ['OTP'],
|
|
1056
|
+
passkey: 'on'
|
|
1057
|
+
}
|
|
1058
|
+
},
|
|
1001
1059
|
{
|
|
1002
1060
|
name: 'send_password_setup',
|
|
1003
1061
|
title: 'Send a password setup email',
|