@faable/faable 4.2.0 → 4.3.0
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/README.md +30 -6
- package/dist/api/FaableApi.js +51 -1
- package/dist/api/auth.js +1 -5
- package/dist/api/auth_admin.js +6 -9
- package/dist/api/base_client.js +4 -1
- package/dist/api/client_id.js +22 -0
- package/dist/api/context.js +3 -5
- package/dist/api/session.js +2 -2
- package/dist/commands/auth/actions/rm.js +12 -15
- package/dist/commands/auth/clients/rm.js +17 -15
- package/dist/commands/auth/users/export.js +6 -12
- package/dist/commands/auth/users/import.js +5 -9
- package/dist/commands/auth/users/reinstate.js +10 -17
- package/dist/commands/auth/users/suspend.js +10 -17
- package/dist/commands/deploy/apps/create.js +132 -0
- package/dist/commands/deploy/apps/index.js +18 -2
- package/dist/commands/deploy/apps/set.js +70 -0
- package/dist/commands/deploy/domains/add.js +8 -2
- package/dist/commands/deploy/domains/check.js +8 -3
- package/dist/commands/deploy/domains/list.js +1 -1
- package/dist/commands/deploy/domains/rm.js +14 -17
- package/dist/commands/deploy/index.js +5 -0
- package/dist/commands/deploy/inspect/cancel.js +9 -3
- package/dist/commands/deploy/inspect/deployments.js +1 -1
- package/dist/commands/deploy/inspect/format.js +35 -1
- package/dist/commands/deploy/inspect/inspect.js +1 -1
- package/dist/commands/deploy/inspect/logs.js +47 -6
- package/dist/commands/deploy/inspect/open.js +1 -1
- package/dist/commands/deploy/inspect/redeploy.js +11 -3
- package/dist/commands/deploy/inspect/status.js +9 -3
- package/dist/commands/deploy/inspect/traffic.js +84 -0
- package/dist/commands/deploy/inspect/trigger.js +10 -4
- package/dist/commands/deploy/launch.js +26 -17
- package/dist/commands/deploy/project_usage.js +83 -0
- package/dist/commands/deploy/resolve_app_id.js +42 -4
- package/dist/commands/deploy/secrets/list.js +1 -1
- package/dist/commands/deploy/secrets/merge.js +4 -2
- package/dist/commands/deploy/secrets/rm.js +14 -17
- package/dist/commands/deploy/secrets/set.js +35 -9
- package/dist/commands/deploy/waf/add_rule.js +4 -1
- package/dist/commands/deploy/waf/block.js +8 -4
- package/dist/commands/deploy/waf/list.js +1 -1
- package/dist/commands/deploy/waf/rm.js +6 -3
- package/dist/commands/deploy/waf/sink.js +8 -4
- package/dist/commands/whoami/index.js +4 -6
- package/dist/index.js +32 -17
- package/dist/lib/errors.js +31 -0
- package/dist/lib/interactive.js +45 -0
- package/dist/lib/listing.js +5 -1
- package/package.json +1 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { requireApi } from '../../../api/context.js';
|
|
2
|
+
import { print_json, json_option } from '../../../lib/listing.js';
|
|
2
3
|
import { resolve_app_id } from '../resolve_app_id.js';
|
|
3
4
|
import { add_rule } from './add_rule.js';
|
|
4
5
|
|
|
5
6
|
const waf_block = {
|
|
6
7
|
command: 'block [pattern]',
|
|
7
8
|
describe: 'Block requests at the edge with a 403 (they never reach your app)',
|
|
8
|
-
builder: yargs => yargs
|
|
9
|
+
builder: yargs => json_option(yargs)
|
|
9
10
|
.positional('pattern', {
|
|
10
11
|
type: 'string',
|
|
11
12
|
description: 'Anchored path regex, e.g. ^/\\.well-known/ (quote it in your shell)'
|
|
@@ -23,7 +24,7 @@ const waf_block = {
|
|
|
23
24
|
.option('app', {
|
|
24
25
|
alias: 'a',
|
|
25
26
|
type: 'string',
|
|
26
|
-
description: 'App
|
|
27
|
+
description: 'App id, name or slug (defaults to the linked app)'
|
|
27
28
|
})
|
|
28
29
|
.option('description', {
|
|
29
30
|
alias: 'd',
|
|
@@ -56,7 +57,7 @@ const waf_block = {
|
|
|
56
57
|
const ctx = await requireApi();
|
|
57
58
|
const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
|
|
58
59
|
const app = await ctx.api.getApp(app_id);
|
|
59
|
-
await add_rule({
|
|
60
|
+
const waf = await add_rule({
|
|
60
61
|
api: ctx.api,
|
|
61
62
|
app_id,
|
|
62
63
|
app_name: app.name,
|
|
@@ -66,8 +67,11 @@ const waf_block = {
|
|
|
66
67
|
query: args.query,
|
|
67
68
|
action: 'deny',
|
|
68
69
|
description: args.description,
|
|
69
|
-
force: args.force
|
|
70
|
+
force: args.force,
|
|
71
|
+
json: args.json
|
|
70
72
|
});
|
|
73
|
+
if (args.json)
|
|
74
|
+
print_json(waf);
|
|
71
75
|
}
|
|
72
76
|
};
|
|
73
77
|
|
|
@@ -11,7 +11,7 @@ const waf_list = {
|
|
|
11
11
|
.option('app', {
|
|
12
12
|
alias: 'a',
|
|
13
13
|
type: 'string',
|
|
14
|
-
description: 'App
|
|
14
|
+
description: 'App id, name or slug (defaults to the linked app)'
|
|
15
15
|
})
|
|
16
16
|
.example('$0 deploy waf list', 'Show the rules protecting the linked app')
|
|
17
17
|
.showHelpOnFail(false),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { requireApi } from '../../../api/context.js';
|
|
2
|
+
import { print_json, json_option } from '../../../lib/listing.js';
|
|
2
3
|
import { log } from '../../../log.js';
|
|
3
4
|
import { resolve_app_id } from '../resolve_app_id.js';
|
|
4
5
|
import { describe_selector } from './add_rule.js';
|
|
@@ -6,7 +7,7 @@ import { describe_selector } from './add_rule.js';
|
|
|
6
7
|
const waf_rm = {
|
|
7
8
|
command: 'rm [pattern]',
|
|
8
9
|
describe: 'Remove one of your WAF rules',
|
|
9
|
-
builder: yargs => yargs
|
|
10
|
+
builder: yargs => json_option(yargs)
|
|
10
11
|
.positional('pattern', {
|
|
11
12
|
type: 'string',
|
|
12
13
|
description: 'The exact pattern to remove (see `faable deploy waf list`)'
|
|
@@ -32,7 +33,7 @@ const waf_rm = {
|
|
|
32
33
|
.option('app', {
|
|
33
34
|
alias: 'a',
|
|
34
35
|
type: 'string',
|
|
35
|
-
description: 'App
|
|
36
|
+
description: 'App id, name or slug (defaults to the linked app)'
|
|
36
37
|
})
|
|
37
38
|
.check(({ pattern, userAgent, query }) => {
|
|
38
39
|
if (!pattern && !userAgent && !query) {
|
|
@@ -47,12 +48,14 @@ const waf_rm = {
|
|
|
47
48
|
const ctx = await requireApi();
|
|
48
49
|
const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
|
|
49
50
|
const app = await ctx.api.getApp(app_id);
|
|
50
|
-
await ctx.api.removeAppWafRule(app_id, {
|
|
51
|
+
const waf = await ctx.api.removeAppWafRule(app_id, {
|
|
51
52
|
pattern: args.pattern,
|
|
52
53
|
user_agent: args.userAgent,
|
|
53
54
|
query: args.query,
|
|
54
55
|
action: args.action
|
|
55
56
|
});
|
|
57
|
+
if (args.json)
|
|
58
|
+
return print_json(waf);
|
|
56
59
|
log.info(`🗑️ Removed ${describe_selector({
|
|
57
60
|
pattern: args.pattern,
|
|
58
61
|
user_agent: args.userAgent,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { requireApi } from '../../../api/context.js';
|
|
2
|
+
import { print_json, json_option } from '../../../lib/listing.js';
|
|
2
3
|
import { resolve_app_id } from '../resolve_app_id.js';
|
|
3
4
|
import { add_rule } from './add_rule.js';
|
|
4
5
|
|
|
5
6
|
const waf_sink = {
|
|
6
7
|
command: 'sink [pattern]',
|
|
7
8
|
describe: 'Answer requests with a 404 from Faable, without waking your app',
|
|
8
|
-
builder: yargs => yargs
|
|
9
|
+
builder: yargs => json_option(yargs)
|
|
9
10
|
.positional('pattern', {
|
|
10
11
|
type: 'string',
|
|
11
12
|
description: 'Anchored path regex, e.g. ^/robots\\.txt$ (quote it in your shell)'
|
|
@@ -23,7 +24,7 @@ const waf_sink = {
|
|
|
23
24
|
.option('app', {
|
|
24
25
|
alias: 'a',
|
|
25
26
|
type: 'string',
|
|
26
|
-
description: 'App
|
|
27
|
+
description: 'App id, name or slug (defaults to the linked app)'
|
|
27
28
|
})
|
|
28
29
|
.option('description', {
|
|
29
30
|
alias: 'd',
|
|
@@ -57,7 +58,7 @@ const waf_sink = {
|
|
|
57
58
|
const ctx = await requireApi();
|
|
58
59
|
const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
|
|
59
60
|
const app = await ctx.api.getApp(app_id);
|
|
60
|
-
await add_rule({
|
|
61
|
+
const waf = await add_rule({
|
|
61
62
|
api: ctx.api,
|
|
62
63
|
app_id,
|
|
63
64
|
app_name: app.name,
|
|
@@ -67,8 +68,11 @@ const waf_sink = {
|
|
|
67
68
|
query: args.query,
|
|
68
69
|
action: 'sink',
|
|
69
70
|
description: args.description,
|
|
70
|
-
force: args.force
|
|
71
|
+
force: args.force,
|
|
72
|
+
json: args.json
|
|
71
73
|
});
|
|
74
|
+
if (args.json)
|
|
75
|
+
print_json(waf);
|
|
72
76
|
}
|
|
73
77
|
};
|
|
74
78
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { log } from '../../log.js';
|
|
2
1
|
import { getMe } from '../../api/auth.js';
|
|
3
2
|
import { resolveTenant } from '../../api/auth_admin.js';
|
|
4
3
|
import { FaableApi } from '../../api/FaableApi.js';
|
|
@@ -7,6 +6,7 @@ import { loadLiveCredentials } from '../../api/session.js';
|
|
|
7
6
|
import { bearer_strategy } from '../../api/strategies/bearer.strategy.js';
|
|
8
7
|
import { CredentialsStore } from '../../lib/CredentialsStore.js';
|
|
9
8
|
import { print_json, print, json_option } from '../../lib/listing.js';
|
|
9
|
+
import { CliError, NOT_LOGGED_IN } from '../../lib/errors.js';
|
|
10
10
|
|
|
11
11
|
// Who you are, and what the CLI acts on: the active project and the Auth
|
|
12
12
|
// tenant `faable auth` would manage. The context is best-effort — a missing
|
|
@@ -50,8 +50,7 @@ const whoami = {
|
|
|
50
50
|
print(`Logged in as: ${config.email} (API key)`);
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
|
-
|
|
54
|
-
process.exit(1);
|
|
53
|
+
throw new CliError("not_logged_in", NOT_LOGGED_IN);
|
|
55
54
|
}
|
|
56
55
|
let me;
|
|
57
56
|
try {
|
|
@@ -59,9 +58,8 @@ const whoami = {
|
|
|
59
58
|
// has no /me route.
|
|
60
59
|
me = await getMe(token);
|
|
61
60
|
}
|
|
62
|
-
catch {
|
|
63
|
-
|
|
64
|
-
process.exit(1);
|
|
61
|
+
catch (e) {
|
|
62
|
+
throw new CliError("session_expired", "Your Faable session has expired or is invalid. Run `faable login` to sign in again.", { status: 401, cause: e });
|
|
65
63
|
}
|
|
66
64
|
const { project, tenant } = await describeContext(token);
|
|
67
65
|
if (args.json) {
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,8 @@ import { version } from './config.js';
|
|
|
12
12
|
import { Configuration } from './lib/Configuration.js';
|
|
13
13
|
import { setProjectFlag, configuredProject } from './api/project.js';
|
|
14
14
|
import { ContextStore } from './lib/ContextStore.js';
|
|
15
|
+
import { CliError, error_json } from './lib/errors.js';
|
|
16
|
+
import { is_non_interactive } from './lib/interactive.js';
|
|
15
17
|
import { notifyIfUpdateAvailable } from './lib/UpdateChecker.js';
|
|
16
18
|
import { log } from './log.js';
|
|
17
19
|
|
|
@@ -27,7 +29,11 @@ const project_badge = async () => {
|
|
|
27
29
|
const { project_name } = await new ContextStore().load();
|
|
28
30
|
return ` · project ${project_name ?? configured.ref}`;
|
|
29
31
|
};
|
|
30
|
-
|
|
32
|
+
// A program is reading: errors go out as JSON on stderr.
|
|
33
|
+
const wants_json = () => process.argv.includes('--json') || is_non_interactive();
|
|
34
|
+
const fail_json = (err) => {
|
|
35
|
+
process.stderr.write(JSON.stringify(error_json(err)) + '\n');
|
|
36
|
+
};
|
|
31
37
|
// yargs re-runs before-validation middlewares once per nested command level
|
|
32
38
|
// (`deploy secrets` = 2 runs), so keep the banner and update check to one.
|
|
33
39
|
let banner_shown = false;
|
|
@@ -46,7 +52,10 @@ yg.scriptName('faable')
|
|
|
46
52
|
// whose stdout IS the export.
|
|
47
53
|
const exports_to_stdout = argv._.slice(0, 3).join(' ') === 'auth users export' && !argv.file;
|
|
48
54
|
// Nor when nobody is watching stderr (a script, the MCP server).
|
|
49
|
-
if (argv.json ||
|
|
55
|
+
if (argv.json ||
|
|
56
|
+
exports_to_stdout ||
|
|
57
|
+
!process.stderr.isTTY ||
|
|
58
|
+
is_non_interactive())
|
|
50
59
|
return;
|
|
51
60
|
log.info(`Faable CLI ${version}${await project_badge()}`);
|
|
52
61
|
// `upgrade` does its own (forced) check
|
|
@@ -58,6 +67,11 @@ yg.scriptName('faable')
|
|
|
58
67
|
alias: 'config',
|
|
59
68
|
description: 'Path to the local `faable.json` file',
|
|
60
69
|
string: true
|
|
70
|
+
})
|
|
71
|
+
.option('non-interactive', {
|
|
72
|
+
description: 'Never prompt nor infer the app from the working directory; errors as JSON (env FAABLE_NONINTERACTIVE=1)',
|
|
73
|
+
boolean: true,
|
|
74
|
+
global: true
|
|
61
75
|
})
|
|
62
76
|
.option('p', {
|
|
63
77
|
alias: 'project',
|
|
@@ -93,16 +107,10 @@ yg.scriptName('faable')
|
|
|
93
107
|
.fail(function (msg, err) {
|
|
94
108
|
if (err) {
|
|
95
109
|
// With --json the caller is a program: the error as JSON too, on
|
|
96
|
-
// stderr, so it can tell a 404 from a 403 without parsing prose
|
|
110
|
+
// stderr, so it can tell a 404 from a 403 without parsing prose
|
|
111
|
+
// (codes in lib/errors.ts).
|
|
97
112
|
if (wants_json()) {
|
|
98
|
-
|
|
99
|
-
process.stderr.write(JSON.stringify({
|
|
100
|
-
error: {
|
|
101
|
-
message: e.message,
|
|
102
|
-
...(e.code ? { code: e.code } : {}),
|
|
103
|
-
...(e.status ? { status: e.status } : {})
|
|
104
|
-
}
|
|
105
|
-
}) + '\n');
|
|
113
|
+
fail_json(err);
|
|
106
114
|
}
|
|
107
115
|
else {
|
|
108
116
|
log.error(`❌ ${err.message}`);
|
|
@@ -111,18 +119,25 @@ yg.scriptName('faable')
|
|
|
111
119
|
return;
|
|
112
120
|
}
|
|
113
121
|
if (msg) {
|
|
114
|
-
// Validation failure (unknown command, missing subcommand…): show the
|
|
115
|
-
// help, then fail red — a bad invocation must not exit 0.
|
|
116
|
-
yg.showHelp();
|
|
117
|
-
log.error(`❌ ${msg}`);
|
|
118
122
|
// The one unknown command worth naming: an app id passed positionally.
|
|
119
123
|
// `faable deploy <app_id> secrets list` used to deploy the working
|
|
120
124
|
// directory instead of listing secrets, so the id now lives in a flag —
|
|
121
125
|
// say so, or the migration reads as the CLI having lost a feature.
|
|
122
126
|
const stray = /Unknown commands?: (app_[A-Za-z0-9_-]+)/.exec(msg);
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
const hint = stray
|
|
128
|
+
? `The app id goes in a flag, not as an argument: faable deploy --app ${stray[1]} [subcommand]`
|
|
129
|
+
: undefined;
|
|
130
|
+
if (wants_json()) {
|
|
131
|
+
// A program built a bad argv: no help screen, just the reason.
|
|
132
|
+
fail_json(new CliError('usage', hint ? `${msg}. ${hint}` : msg));
|
|
133
|
+
process.exit(1);
|
|
125
134
|
}
|
|
135
|
+
// Validation failure (unknown command, missing subcommand…): show the
|
|
136
|
+
// help, then fail red — a bad invocation must not exit 0.
|
|
137
|
+
yg.showHelp();
|
|
138
|
+
log.error(`❌ ${msg}`);
|
|
139
|
+
if (hint)
|
|
140
|
+
log.error(hint);
|
|
126
141
|
process.exit(1);
|
|
127
142
|
}
|
|
128
143
|
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class CliError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
status;
|
|
4
|
+
action;
|
|
5
|
+
constructor(code, message, extra = {}) {
|
|
6
|
+
super(message, extra.cause === undefined ? undefined : { cause: extra.cause });
|
|
7
|
+
this.name = 'CliError';
|
|
8
|
+
this.code = code;
|
|
9
|
+
if (extra.status !== undefined)
|
|
10
|
+
this.status = extra.status;
|
|
11
|
+
if (extra.action !== undefined)
|
|
12
|
+
this.action = extra.action;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
// The JSON body of a failure, from any error: a CliError, a FaableApi error
|
|
16
|
+
// (which carries status/code/action) or a plain Error.
|
|
17
|
+
const error_json = (err) => {
|
|
18
|
+
const e = (err ?? {});
|
|
19
|
+
const message = e.message || String(err);
|
|
20
|
+
return {
|
|
21
|
+
error: {
|
|
22
|
+
message,
|
|
23
|
+
...(e.code ? { code: e.code } : {}),
|
|
24
|
+
...(e.status ? { status: e.status } : {}),
|
|
25
|
+
...(e.action ? { action: e.action } : {})
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const NOT_LOGGED_IN = "Not logged in. Run 'faable login' first.";
|
|
30
|
+
|
|
31
|
+
export { CliError, NOT_LOGGED_IN, error_json };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import { CliError } from './errors.js';
|
|
3
|
+
|
|
4
|
+
// Non-interactive mode: FAABLE_NONINTERACTIVE=1 or the global
|
|
5
|
+
// --non-interactive flag. A program drives the CLI (the Faable MCP server sets
|
|
6
|
+
// it on every call), so:
|
|
7
|
+
//
|
|
8
|
+
// - no prompt is ever shown: a confirmation without --yes is an error
|
|
9
|
+
// (`confirmation_required`), never a silent "Cancelled." with exit 0;
|
|
10
|
+
// - nothing is inferred from the working directory: the app comes from
|
|
11
|
+
// --app, never from faable.json or the git remote of wherever the process
|
|
12
|
+
// happens to run (see find_app_id);
|
|
13
|
+
// - `deploy launch` needs --yes, --app and --workdir spelled out;
|
|
14
|
+
// - errors are JSON on stderr, as with --json.
|
|
15
|
+
//
|
|
16
|
+
// Opt-in rather than "no TTY": CI runs without one and `faable deploy` there
|
|
17
|
+
// must keep deploying unattended.
|
|
18
|
+
const is_non_interactive = (env = process.env, argv = process.argv) => ['1', 'true'].includes((env.FAABLE_NONINTERACTIVE ?? '').toLowerCase()) ||
|
|
19
|
+
argv.includes('--non-interactive');
|
|
20
|
+
// Ask before something destructive. True to go ahead, false when the person
|
|
21
|
+
// said no. Throws `confirmation_required` when nobody can answer: in
|
|
22
|
+
// non-interactive mode, or when stdin/stderr are not a terminal (prompts
|
|
23
|
+
// would otherwise resolve undefined and the command would report a
|
|
24
|
+
// cancellation as a clean exit 0 that a caller reads as success).
|
|
25
|
+
//
|
|
26
|
+
// The prompt is drawn on stderr: stdout carries data only.
|
|
27
|
+
const confirm = async (opts) => {
|
|
28
|
+
if (opts.yes)
|
|
29
|
+
return true;
|
|
30
|
+
if (is_non_interactive() || !process.stdin.isTTY || !process.stderr.isTTY) {
|
|
31
|
+
throw new CliError('confirmation_required', `${opts.message} — confirmation needed: pass --yes.`);
|
|
32
|
+
}
|
|
33
|
+
const { confirm } = await prompts({
|
|
34
|
+
type: 'toggle',
|
|
35
|
+
name: 'confirm',
|
|
36
|
+
message: opts.message,
|
|
37
|
+
initial: false,
|
|
38
|
+
active: 'yes',
|
|
39
|
+
inactive: 'no',
|
|
40
|
+
stdout: process.stderr
|
|
41
|
+
}, { onCancel: () => false });
|
|
42
|
+
return !!confirm;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export { confirm, is_non_interactive };
|
package/dist/lib/listing.js
CHANGED
|
@@ -72,6 +72,10 @@ const print = (line = '') => {
|
|
|
72
72
|
const print_json = (data) => {
|
|
73
73
|
print(JSON.stringify(data, null, 2));
|
|
74
74
|
};
|
|
75
|
+
// A write that had nothing to do (no failed deployment to retry, nothing in
|
|
76
|
+
// flight to cancel). Exit 0 — it is not an error — but said in the data, so a
|
|
77
|
+
// caller never mistakes it for the thing having happened.
|
|
78
|
+
const print_noop = (reason) => print_json({ result: 'noop', reason });
|
|
75
79
|
// The rerun hint for the next page, on stderr (the logger's): shown to a
|
|
76
80
|
// person, invisible to a pipe.
|
|
77
81
|
const more_hint = (page) => page.has_more
|
|
@@ -88,4 +92,4 @@ const table_lines = (headers, rows) => {
|
|
|
88
92
|
return [render(headers), ...rows.map(render)];
|
|
89
93
|
};
|
|
90
94
|
|
|
91
|
-
export { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, as_list, fetch_page, from_paginator, json_option, list_options, more_hint, print, print_json, table_lines };
|
|
95
|
+
export { DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE, as_list, fetch_page, from_paginator, json_option, list_options, more_hint, print, print_json, print_noop, table_lines };
|