@faable/faable 4.1.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 +37 -0
- 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 +8 -2
- 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
package/README.md
CHANGED
|
@@ -28,8 +28,10 @@ npm i -g @faable/faable
|
|
|
28
28
|
| Group | What it does | Docs |
|
|
29
29
|
| --- | --- | --- |
|
|
30
30
|
| `faable login` / `whoami` / `logout` | Session management (device flow; CI uses OIDC, no login needed) | [Authentication](https://faable.com/docs/cli#authentication) |
|
|
31
|
-
| `faable
|
|
32
|
-
| `faable deploy
|
|
31
|
+
| `faable project` | The project commands act on: `list`, `use`, `current`, `clear` (or `--project` on any command) | [Projects](https://faable.com/docs/cli#projects) |
|
|
32
|
+
| `faable deploy` | `launch` (the default: a bare `faable deploy` deploys the current directory), then `status`, `logs`, `deployments`, `inspect`, `trigger`, `redeploy`, `cancel`, `traffic`, `usage`, `quota`, `open` | [Deployment](https://faable.com/docs/cli#deployment) |
|
|
33
|
+
| `faable deploy apps` | `list`, `get`, `create` (from a GitHub repository, first deploy included), `set` (branch, root directory, deploy mode) | [Apps](https://faable.com/docs/cli#apps) |
|
|
34
|
+
| `faable deploy secrets` | Environment variables: `list`, `set` (`KEY=VALUE`, `--env-file`, or `-f -` from stdin), `rm` | [Secrets](https://faable.com/docs/cli#secrets) |
|
|
33
35
|
| `faable deploy domains` | Custom domains: `add` (prints the CNAME), `list`, `check`, `rm` | [Domains](https://faable.com/docs/cli#domains) |
|
|
34
36
|
| `faable deploy waf` | Edge rules: `block` (403), `sink` (404 without waking the app), `list`, `rm` | [Edge rules](https://faable.com/docs/cli#edge-rules-waf) |
|
|
35
37
|
| `faable auth users` | `list` (FaableQL filters), `get` (federated identities included), `suspend`, `reinstate` | [Users](https://faable.com/docs/cli#users) |
|
|
@@ -37,18 +39,40 @@ npm i -g @faable/faable
|
|
|
37
39
|
| `faable auth clients` | OAuth clients: `list`, `get --secret`, `create`, `rm` | [OAuth clients](https://faable.com/docs/cli#oauth-clients) |
|
|
38
40
|
| `faable auth logs` | Audit log (read-only): `list` with filters, `get` | [Audit logs](https://faable.com/docs/cli#audit-logs) |
|
|
39
41
|
|
|
40
|
-
Every command has `--help
|
|
41
|
-
|
|
42
|
+
Every command has `--help` and `--json`. `--app` takes an app id, name or slug.
|
|
43
|
+
The complete table is in the
|
|
42
44
|
[command reference](https://faable.com/docs/cli#command-reference).
|
|
43
45
|
|
|
46
|
+
### Scripting and agents
|
|
47
|
+
|
|
48
|
+
Data goes to stdout, messages to stderr. With `--json`:
|
|
49
|
+
|
|
50
|
+
- every listing is `{"object":"list","data":[…],"has_more","next_cursor"}`
|
|
51
|
+
(`--limit`, `--starting-after`, `--all`);
|
|
52
|
+
- every write returns what it changed, and a write with nothing to do returns
|
|
53
|
+
`{"result":"noop","reason"}`;
|
|
54
|
+
- a failure exits 1 with `{"error":{"message","code","status","action"}}` on
|
|
55
|
+
stderr. Codes: `not_logged_in`, `session_expired`, `account_suspended`,
|
|
56
|
+
`apikey_session`, `forbidden`, `not_found`, `confirmation_required`,
|
|
57
|
+
`app_required`, `usage`, or the API's own code.
|
|
58
|
+
|
|
59
|
+
A program driving the CLI should set `FAABLE_NONINTERACTIVE=1` (or pass
|
|
60
|
+
`--non-interactive`): nothing prompts — a confirmation without `--yes` fails
|
|
61
|
+
with `confirmation_required` —, the app is never inferred from the working
|
|
62
|
+
directory, and `faable deploy launch` needs `--app`, `--workdir` and `--yes`.
|
|
63
|
+
Pass secret values on stdin (`faable deploy secrets set -f -`), never as
|
|
64
|
+
arguments.
|
|
65
|
+
|
|
44
66
|
### Deploy
|
|
45
67
|
|
|
46
68
|
```bash
|
|
47
69
|
faable login
|
|
48
70
|
faable deploy # deploy the current directory (= deploy launch)
|
|
49
71
|
faable deploy launch --app app_x # deploy another app, from anywhere
|
|
72
|
+
faable deploy apps create --repo acme/web # create, link, first deploy
|
|
50
73
|
faable deploy status # phase, URL and detected stack
|
|
51
|
-
faable deploy logs --build
|
|
74
|
+
faable deploy logs --build -n 100 # end of the build output (--follow to tail)
|
|
75
|
+
faable deploy traffic --since 7d # status codes, top and failing paths
|
|
52
76
|
faable deploy secrets set KEY=value
|
|
53
77
|
faable deploy domains add app.example.com
|
|
54
78
|
```
|
|
@@ -65,7 +89,7 @@ faable auth users get user_abc123 # includes GitHub/… identi
|
|
|
65
89
|
faable auth users suspend user_abc123 -r "abuse: crypto miner"
|
|
66
90
|
|
|
67
91
|
# Bulk: pipe ids from a filtered listing
|
|
68
|
-
faable auth users list --suspended --json | jq -r '.[].id' | faable auth users reinstate -y
|
|
92
|
+
faable auth users list --suspended --json | jq -r '.data[].id' | faable auth users reinstate -y
|
|
69
93
|
|
|
70
94
|
faable auth actions create -n add-claims -t post-login -f ./claims.js
|
|
71
95
|
faable auth clients create -n my-app --callback https://app.example.com/callback
|
package/dist/api/FaableApi.js
CHANGED
|
@@ -90,13 +90,19 @@ class FaableApi {
|
|
|
90
90
|
if (res.status === 401) {
|
|
91
91
|
const expired = new Error('Your Faable session has expired or is invalid. Run `faable login` to sign in again.', { cause: error });
|
|
92
92
|
expired.status = 401;
|
|
93
|
+
expired.code = 'session_expired';
|
|
93
94
|
throw expired;
|
|
94
95
|
}
|
|
95
96
|
const serverMessage = res.data?.message || res.statusText || 'Unknown Error';
|
|
96
97
|
const wrapped = new Error(`FaableApi ${url} ${res.status}: ${serverMessage}`, { cause: error });
|
|
97
98
|
wrapped.status = res.status;
|
|
99
|
+
// The api's own code wins; a bare 403/404 still gets a stable one.
|
|
98
100
|
if (res.data?.code)
|
|
99
101
|
wrapped.code = res.data.code;
|
|
102
|
+
else if (res.status === 403)
|
|
103
|
+
wrapped.code = 'forbidden';
|
|
104
|
+
else if (res.status === 404)
|
|
105
|
+
wrapped.code = 'not_found';
|
|
100
106
|
if (res.data?.action)
|
|
101
107
|
wrapped.action = res.data.action;
|
|
102
108
|
throw wrapped;
|
|
@@ -254,7 +260,10 @@ class FaableApi {
|
|
|
254
260
|
// Build and deploy the current head of the deploy branch server-side —
|
|
255
261
|
// the same path a push webhook takes, same-commit dedupe included.
|
|
256
262
|
async deployNow(app_id) {
|
|
257
|
-
return data(
|
|
263
|
+
return data(
|
|
264
|
+
// deployment_id: the build this call started (api ≥ the release that
|
|
265
|
+
// added it; absent from older ones).
|
|
266
|
+
this.client.post(`/app/${app_id}/deploy`));
|
|
258
267
|
}
|
|
259
268
|
// Rebuild a failed deployment from its recorded source (CAS manifest or
|
|
260
269
|
// git ref). The API enforces the guards: failed phase only, never older
|
|
@@ -296,6 +305,47 @@ class FaableApi {
|
|
|
296
305
|
headers: projectHeader(team)
|
|
297
306
|
}));
|
|
298
307
|
}
|
|
308
|
+
// ── apps: create, configure, observe ─────────────────────────────────────
|
|
309
|
+
// A bare app in a project; `linkRepository` gives it something to build.
|
|
310
|
+
async createApp(project, params) {
|
|
311
|
+
return data(this.client.post(`/app`, params, {
|
|
312
|
+
headers: projectHeader(project)
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
async deleteApp(app_id, team) {
|
|
316
|
+
return data(this.client.delete(`/app/${app_id}`, { headers: projectHeader(team) }));
|
|
317
|
+
}
|
|
318
|
+
async setDeployBranch(app_id, github_branch) {
|
|
319
|
+
return data(this.client.post(`/app/${app_id}/deploy-branch`, { github_branch }));
|
|
320
|
+
}
|
|
321
|
+
// null clears the platform override (faable.json's rootDir applies again).
|
|
322
|
+
async setRootDir(app_id, root_dir) {
|
|
323
|
+
return data(this.client.post(`/app/${app_id}/root-dir`, { root_dir }));
|
|
324
|
+
}
|
|
325
|
+
// push: every push deploys · ci: deploys once CI tags a release ·
|
|
326
|
+
// workflow: the repo's own GitHub workflow runs `faable deploy`.
|
|
327
|
+
async setDeployMode(app_id, mode) {
|
|
328
|
+
return data(this.client.post(`/app/${app_id}/deploy-mode`, { mode }));
|
|
329
|
+
}
|
|
330
|
+
// Edge traffic of one app (ClickHouse, trails reality by up to 15 min).
|
|
331
|
+
// from/to are unix seconds.
|
|
332
|
+
async getAppTraffic(app_id, team, params = {}) {
|
|
333
|
+
return data(this.client.get(`/app/${app_id}/traffic`, {
|
|
334
|
+
params,
|
|
335
|
+
headers: projectHeader(team)
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
// The project's usage this billing period. `traffic: false` keeps it to
|
|
339
|
+
// the database (the Stripe-backed traffic figures are the slow half).
|
|
340
|
+
async getUsageSummary(project, params = {}) {
|
|
341
|
+
return data(this.client.get(`/usage/summary`, {
|
|
342
|
+
params,
|
|
343
|
+
headers: projectHeader(project)
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
async getDeployQuota(project) {
|
|
347
|
+
return data(this.client.get(`/deploy-quota`, { headers: projectHeader(project) }));
|
|
348
|
+
}
|
|
299
349
|
// ── per-app WAF ───────────────────────────────────────────────────────────
|
|
300
350
|
//
|
|
301
351
|
// No `x-faable-project` header on any of these: the routes are scoped by the
|
package/dist/api/auth.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FaableAuthApi, authBearer } from '@faable/auth-sdk';
|
|
2
2
|
import os from 'os';
|
|
3
|
-
import {
|
|
3
|
+
import { CLI_CLIENT } from './client_id.js';
|
|
4
4
|
|
|
5
5
|
// The CLI talks to the Faable tenant's auth server through the same SDK the
|
|
6
6
|
// rest of the platform uses. It used to be a bare axios instance, which meant
|
|
@@ -18,10 +18,6 @@ import { version } from '../config.js';
|
|
|
18
18
|
// whatever `loadLiveCredentials` refreshed is what goes on the wire.
|
|
19
19
|
const AUTH_DOMAIN = "https://faable.auth.faable.link";
|
|
20
20
|
const CLIENT_ID = "c879023b-e34f-4b0c-a262-210e556bc2e4";
|
|
21
|
-
// `<name>/<version>`. No commit: the CLI has no release SHA constant, and a
|
|
22
|
-
// dev build (`0.0.0-development`) is identifiable as such by the version
|
|
23
|
-
// alone. auth caps this segment at 32 chars.
|
|
24
|
-
const CLI_CLIENT = `faable-cli/${version}`;
|
|
25
21
|
// No `x-faable-instance`: the SDK's default is `${HOSTNAME}:${pid}`, which on
|
|
26
22
|
// a laptop is the user's machine name in our audit log. An empty string
|
|
27
23
|
// suppresses the header (the helper drops falsy parts).
|
package/dist/api/auth_admin.js
CHANGED
|
@@ -3,7 +3,7 @@ import os from 'os';
|
|
|
3
3
|
import path__default from 'path';
|
|
4
4
|
import { ContextStore } from '../lib/ContextStore.js';
|
|
5
5
|
import { CredentialsStore } from '../lib/CredentialsStore.js';
|
|
6
|
-
import {
|
|
6
|
+
import { CliError, NOT_LOGGED_IN } from '../lib/errors.js';
|
|
7
7
|
import { FaableApi } from './FaableApi.js';
|
|
8
8
|
import { AUTH_DOMAIN, createAnonymousAuthApi, createBearerAuthApi } from './auth.js';
|
|
9
9
|
import { requireProject } from './project.js';
|
|
@@ -17,13 +17,10 @@ const requireSessionToken = async () => {
|
|
|
17
17
|
const config = await loadLiveCredentials(new CredentialsStore());
|
|
18
18
|
if (config?.apikey && !config.token) {
|
|
19
19
|
// Deploy API keys are not Auth management credentials.
|
|
20
|
-
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
23
|
-
if (!config?.token) {
|
|
24
|
-
log.error("❌ Not logged in. Run 'faable login' first.");
|
|
25
|
-
process.exit(1);
|
|
20
|
+
throw new CliError('apikey_session', "You are logged in with an API key, but `faable auth` needs a browser session. Run 'faable login' (without --apikey) first.");
|
|
26
21
|
}
|
|
22
|
+
if (!config?.token)
|
|
23
|
+
throw new CliError('not_logged_in', NOT_LOGGED_IN);
|
|
27
24
|
return config.token;
|
|
28
25
|
};
|
|
29
26
|
// Which Auth tenant `faable auth` manages, in precedence order:
|
|
@@ -193,10 +190,10 @@ const issueTenantToken = async (session_token, account_id, api = FaableApi.creat
|
|
|
193
190
|
const hintAuthError = (e) => {
|
|
194
191
|
const status = e?.response?.status;
|
|
195
192
|
if (status === 401) {
|
|
196
|
-
throw new
|
|
193
|
+
throw new CliError('session_expired', "Unauthorized (401) by the Auth management API. Your session may have expired — run 'faable login' and retry.", { status, cause: e });
|
|
197
194
|
}
|
|
198
195
|
if (status === 403) {
|
|
199
|
-
throw new
|
|
196
|
+
throw new CliError('forbidden', 'Forbidden (403): your session is not allowed to manage this tenant. Check the tenant (`faable whoami`, --account, --project), or use credentials with management access for it.', { status, cause: e });
|
|
200
197
|
}
|
|
201
198
|
throw e;
|
|
202
199
|
};
|
package/dist/api/base_client.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
+
import { CLI_CLIENT } from './client_id.js';
|
|
2
3
|
|
|
3
4
|
// FAABLE_API_URL: point the CLI at another Deploy API frontend (e.g.
|
|
4
5
|
// https://deploy.staging.faable.com for staging-gated testing). BASE_URL is
|
|
@@ -9,7 +10,9 @@ const BASE_URL = process.env.FAABLE_API_URL ||
|
|
|
9
10
|
const create_base_client = () => {
|
|
10
11
|
return axios.create({
|
|
11
12
|
baseURL: BASE_URL,
|
|
12
|
-
timeout: 10000
|
|
13
|
+
timeout: 10000,
|
|
14
|
+
// Who is calling: the api tags its request log by this name.
|
|
15
|
+
headers: { "x-faable-client": CLI_CLIENT }
|
|
13
16
|
});
|
|
14
17
|
};
|
|
15
18
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { version } from '../config.js';
|
|
2
|
+
|
|
3
|
+
// What the CLI says it is, in `x-faable-client` on every request — to the
|
|
4
|
+
// Deploy API (base_client.ts) and to the auth server (auth.ts) alike. The api
|
|
5
|
+
// tags its request log by the name before the `/`, so traffic from the CLI —
|
|
6
|
+
// and from the MCP server driving it — can be counted apart.
|
|
7
|
+
//
|
|
8
|
+
// `<name>/<version>`. No commit: the CLI has no release SHA constant, and a
|
|
9
|
+
// dev build (`0.0.0-development`) is identifiable as such by the version
|
|
10
|
+
// alone. A `+suffix` would be read as a commit sha, so the driver is told by
|
|
11
|
+
// the name, not by a suffix. auth caps the version segment at 32 chars.
|
|
12
|
+
//
|
|
13
|
+
// FAABLE_CLIENT_NAME: who drives this CLI when it isn't a person. The Faable
|
|
14
|
+
// MCP server sets `faable-mcp`. Only `faable-*` names: anything else is
|
|
15
|
+
// ignored rather than letting an environment impersonate another client.
|
|
16
|
+
const client_name = (env = process.env) => {
|
|
17
|
+
const name = env.FAABLE_CLIENT_NAME?.trim().toLowerCase() ?? '';
|
|
18
|
+
return /^faable-[a-z0-9-]{1,24}$/.test(name) ? name : 'faable-cli';
|
|
19
|
+
};
|
|
20
|
+
const CLI_CLIENT = `${client_name()}/${version}`;
|
|
21
|
+
|
|
22
|
+
export { CLI_CLIENT, client_name };
|
package/dist/api/context.js
CHANGED
|
@@ -5,8 +5,8 @@ import { oidc_strategy } from './strategies/oidc.strategy.js';
|
|
|
5
5
|
import { bearer_strategy } from './strategies/bearer.strategy.js';
|
|
6
6
|
import { CredentialsStore } from '../lib/CredentialsStore.js';
|
|
7
7
|
import { loadLiveCredentials } from './session.js';
|
|
8
|
-
import { log } from '../log.js';
|
|
9
8
|
import { configuredProject, resolveProjectRef } from './project.js';
|
|
9
|
+
import { CliError, NOT_LOGGED_IN } from '../lib/errors.js';
|
|
10
10
|
|
|
11
11
|
const context = async (targetAppId) => {
|
|
12
12
|
let api;
|
|
@@ -70,10 +70,8 @@ const context = async (targetAppId) => {
|
|
|
70
70
|
// whose first call returns 401 — FaableApi maps that to the same re-login hint.
|
|
71
71
|
const requireApi = async (targetAppId) => {
|
|
72
72
|
const ctx = await context(targetAppId);
|
|
73
|
-
if (!ctx.api)
|
|
74
|
-
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
73
|
+
if (!ctx.api)
|
|
74
|
+
throw new CliError("not_logged_in", NOT_LOGGED_IN);
|
|
77
75
|
return ctx;
|
|
78
76
|
};
|
|
79
77
|
|
package/dist/api/session.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CredentialsStore } from '../lib/CredentialsStore.js';
|
|
2
2
|
import { refreshToken } from './auth.js';
|
|
3
3
|
import { log } from '../log.js';
|
|
4
|
+
import { CliError } from '../lib/errors.js';
|
|
4
5
|
|
|
5
6
|
// True when a stored JWT access token is still valid (has a future `exp`).
|
|
6
7
|
// Anything we can't decode, or without `exp`, counts as NOT live — callers then
|
|
@@ -54,8 +55,7 @@ const loadLiveCredentials = async (store = new CredentialsStore()) => {
|
|
|
54
55
|
const body = e
|
|
55
56
|
?.response?.data;
|
|
56
57
|
if (body?.error_code === "user_suspended") {
|
|
57
|
-
|
|
58
|
-
process.exit(1);
|
|
58
|
+
throw new CliError("account_suspended", "Your account has been suspended. Contact support@faable.com.");
|
|
59
59
|
}
|
|
60
60
|
// Refresh failed — keep the stale config; the API call's 401 (or
|
|
61
61
|
// requireApi) will tell the user to run `faable login` again.
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import prompts from 'prompts';
|
|
2
1
|
import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
|
|
3
2
|
import { log } from '../../../log.js';
|
|
3
|
+
import { json_option, print_json } from '../../../lib/listing.js';
|
|
4
4
|
import { formatTriggers } from './triggers.js';
|
|
5
5
|
import { tenant_options } from '../options.js';
|
|
6
|
+
import { confirm } from '../../../lib/interactive.js';
|
|
6
7
|
|
|
7
8
|
const actions_rm = {
|
|
8
9
|
command: 'rm <action_id>',
|
|
9
10
|
describe: 'Delete an action',
|
|
10
|
-
builder: yargs => tenant_options(yargs)
|
|
11
|
+
builder: yargs => json_option(tenant_options(yargs))
|
|
11
12
|
.positional('action_id', {
|
|
12
13
|
type: 'string',
|
|
13
14
|
demandOption: true,
|
|
@@ -24,21 +25,17 @@ const actions_rm = {
|
|
|
24
25
|
const api = await requireAuthAdmin(args);
|
|
25
26
|
// Fetch first: shows WHAT will be deleted and 404s before the prompt.
|
|
26
27
|
const action = await api.actionGet(args.action_id);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
inactive: 'no'
|
|
35
|
-
});
|
|
36
|
-
if (!confirm) {
|
|
37
|
-
log.info('Cancelled.');
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
28
|
+
const go = await confirm({
|
|
29
|
+
message: `Delete action "${action.name}" (${action.id}, triggers ${formatTriggers(action)})?`,
|
|
30
|
+
yes: args.yes
|
|
31
|
+
});
|
|
32
|
+
if (!go) {
|
|
33
|
+
log.info('Cancelled.');
|
|
34
|
+
return;
|
|
40
35
|
}
|
|
41
36
|
await api.actionDelete(args.action_id);
|
|
37
|
+
if (args.json)
|
|
38
|
+
return print_json({ deleted: true, id: args.action_id });
|
|
42
39
|
log.info(`🗑️ Deleted action ${args.action_id}.`);
|
|
43
40
|
})
|
|
44
41
|
};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import prompts from 'prompts';
|
|
2
1
|
import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
|
|
3
2
|
import { log } from '../../../log.js';
|
|
3
|
+
import { json_option, print_json } from '../../../lib/listing.js';
|
|
4
4
|
import { tenant_options } from '../options.js';
|
|
5
5
|
import { resolve_client } from './resolve.js';
|
|
6
|
+
import { confirm } from '../../../lib/interactive.js';
|
|
6
7
|
|
|
7
8
|
const clients_rm = {
|
|
8
9
|
command: 'rm <client_id>',
|
|
9
10
|
describe: 'Delete an OAuth client',
|
|
10
|
-
builder: yargs => tenant_options(yargs)
|
|
11
|
+
builder: yargs => json_option(tenant_options(yargs))
|
|
11
12
|
.positional('client_id', {
|
|
12
13
|
type: 'string',
|
|
13
14
|
demandOption: true,
|
|
@@ -23,21 +24,22 @@ const clients_rm = {
|
|
|
23
24
|
handler: withAuthHints(async (args) => {
|
|
24
25
|
const api = await requireAuthAdmin(args);
|
|
25
26
|
const client = await resolve_client(api, args.client_id);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
inactive: 'no'
|
|
34
|
-
});
|
|
35
|
-
if (!confirm) {
|
|
36
|
-
log.info('Cancelled.');
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
27
|
+
const go = await confirm({
|
|
28
|
+
message: `Delete client "${client.name}" (${client.client_id})? Apps using it will stop authenticating.`,
|
|
29
|
+
yes: args.yes
|
|
30
|
+
});
|
|
31
|
+
if (!go) {
|
|
32
|
+
log.info('Cancelled.');
|
|
33
|
+
return;
|
|
39
34
|
}
|
|
40
35
|
await api.clientDelete(client.id);
|
|
36
|
+
if (args.json) {
|
|
37
|
+
return print_json({
|
|
38
|
+
deleted: true,
|
|
39
|
+
id: client.id,
|
|
40
|
+
client_id: client.client_id
|
|
41
|
+
});
|
|
42
|
+
}
|
|
41
43
|
log.info(`🗑️ Deleted client ${client.client_id} (${client.id}).`);
|
|
42
44
|
})
|
|
43
45
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { openSync, fchmodSync, writeSync, closeSync } from 'node:fs';
|
|
2
|
-
import prompts from 'prompts';
|
|
3
2
|
import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
|
|
4
3
|
import { log } from '../../../log.js';
|
|
5
4
|
import { tenant_options } from '../options.js';
|
|
5
|
+
import { confirm } from '../../../lib/interactive.js';
|
|
6
6
|
|
|
7
7
|
// GET /user/export pages at most this many users.
|
|
8
8
|
const PAGE_SIZE = 500;
|
|
@@ -45,18 +45,12 @@ const users_export = {
|
|
|
45
45
|
if (hashes && to_stdout && process.stdout.isTTY && !args.force) {
|
|
46
46
|
throw new Error('Refusing to print password hashes to a terminal: pass a file, pipe stdout, or add --force');
|
|
47
47
|
}
|
|
48
|
-
if (hashes
|
|
49
|
-
|
|
50
|
-
const { confirm } = await prompts({
|
|
51
|
-
type: 'toggle',
|
|
52
|
-
name: 'confirm',
|
|
48
|
+
if (hashes) {
|
|
49
|
+
const go = await confirm({
|
|
53
50
|
message: 'Export password hashes? Anyone holding the file can attack every password offline. The export is audited and notifies the tenant subscribers.',
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
stdout: process.stderr
|
|
58
|
-
}, { onCancel: () => false });
|
|
59
|
-
if (!confirm) {
|
|
51
|
+
yes: args.yes
|
|
52
|
+
});
|
|
53
|
+
if (!go) {
|
|
60
54
|
process.stderr.write('Cancelled.\n');
|
|
61
55
|
return;
|
|
62
56
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import prompts from 'prompts';
|
|
3
2
|
import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
|
|
4
3
|
import { log } from '../../../log.js';
|
|
5
4
|
import { tenant_options } from '../options.js';
|
|
6
5
|
import { json_option, print_json } from '../../../lib/listing.js';
|
|
7
6
|
import { SOURCES, ADAPTERS, SOURCE_FORMAT } from './import/adapters.js';
|
|
8
7
|
import { parse_records } from './import/records.js';
|
|
8
|
+
import { confirm } from '../../../lib/interactive.js';
|
|
9
9
|
|
|
10
10
|
// The server takes at most this many rows per request (POST /user/import).
|
|
11
11
|
const BATCH_SIZE = 500;
|
|
@@ -119,16 +119,12 @@ const users_import = {
|
|
|
119
119
|
throw new Error(`No importable users in ${args.file} (${total} record(s) read)`);
|
|
120
120
|
}
|
|
121
121
|
const dry_run = !!args['dry-run'];
|
|
122
|
-
if (!
|
|
123
|
-
const
|
|
124
|
-
type: 'toggle',
|
|
125
|
-
name: 'confirm',
|
|
122
|
+
if (!dry_run) {
|
|
123
|
+
const go = await confirm({
|
|
126
124
|
message: `Import ${rows.length} user(s) from ${args.from}?`,
|
|
127
|
-
|
|
128
|
-
active: 'yes',
|
|
129
|
-
inactive: 'no'
|
|
125
|
+
yes: args.yes
|
|
130
126
|
});
|
|
131
|
-
if (!
|
|
127
|
+
if (!go) {
|
|
132
128
|
log.info('Cancelled.');
|
|
133
129
|
return;
|
|
134
130
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import prompts from 'prompts';
|
|
2
1
|
import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
|
|
3
2
|
import { log } from '../../../log.js';
|
|
4
3
|
import { tenant_options } from '../options.js';
|
|
5
4
|
import { json_option, print_json } from '../../../lib/listing.js';
|
|
6
5
|
import { wants_stdin, read_stdin, parse_user_ids } from './ids.js';
|
|
6
|
+
import { confirm } from '../../../lib/interactive.js';
|
|
7
7
|
|
|
8
8
|
const users_reinstate = {
|
|
9
9
|
command: 'reinstate [user_ids..]',
|
|
@@ -21,7 +21,7 @@ const users_reinstate = {
|
|
|
21
21
|
description: 'Skip the confirmation prompt'
|
|
22
22
|
})
|
|
23
23
|
.example('$0 auth users reinstate user_abc123', 'Reinstate one user')
|
|
24
|
-
.example('faable auth users list --suspended --json | jq -r ".[].id" | $0 auth users reinstate -y', 'Bulk-reinstate ids piped from a filtered listing')
|
|
24
|
+
.example('faable auth users list --suspended --json | jq -r ".data[].id" | $0 auth users reinstate -y', 'Bulk-reinstate ids piped from a filtered listing')
|
|
25
25
|
.showHelpOnFail(false),
|
|
26
26
|
handler: withAuthHints(async (args) => {
|
|
27
27
|
const argv_ids = args.user_ids ?? [];
|
|
@@ -29,21 +29,14 @@ const users_reinstate = {
|
|
|
29
29
|
? await read_stdin()
|
|
30
30
|
: null;
|
|
31
31
|
const ids = parse_user_ids(argv_ids, stdin);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
active: 'yes',
|
|
41
|
-
inactive: 'no'
|
|
42
|
-
});
|
|
43
|
-
if (!confirm) {
|
|
44
|
-
log.info('Cancelled.');
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
32
|
+
const preview = ids.slice(0, 5).join(', ') + (ids.length > 5 ? ', …' : '');
|
|
33
|
+
const go = await confirm({
|
|
34
|
+
message: `Reinstate ${ids.length} user(s) (${preview})?`,
|
|
35
|
+
yes: args.yes
|
|
36
|
+
});
|
|
37
|
+
if (!go) {
|
|
38
|
+
log.info('Cancelled.');
|
|
39
|
+
return;
|
|
47
40
|
}
|
|
48
41
|
const api = await requireAuthAdmin(args);
|
|
49
42
|
const results = [];
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import prompts from 'prompts';
|
|
2
1
|
import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
|
|
3
2
|
import { log } from '../../../log.js';
|
|
4
3
|
import { tenant_options } from '../options.js';
|
|
5
4
|
import { json_option, print_json } from '../../../lib/listing.js';
|
|
6
5
|
import { wants_stdin, read_stdin, parse_user_ids } from './ids.js';
|
|
6
|
+
import { confirm } from '../../../lib/interactive.js';
|
|
7
7
|
|
|
8
8
|
const users_suspend = {
|
|
9
9
|
command: 'suspend [user_ids..]',
|
|
@@ -27,7 +27,7 @@ const users_suspend = {
|
|
|
27
27
|
})
|
|
28
28
|
.example('$0 auth users suspend user_abc123 --reason "abuse: proxy payload"', 'Suspend one user')
|
|
29
29
|
.example('$0 auth users suspend user_a user_b user_c -y -r "abuse wave"', 'Suspend several users without prompting')
|
|
30
|
-
.example('faable auth users list --query email_verified:false --json | jq -r ".[].id" | $0 auth users suspend -y', 'Bulk-suspend ids piped from a filtered listing')
|
|
30
|
+
.example('faable auth users list --query email_verified:false --json | jq -r ".data[].id" | $0 auth users suspend -y', 'Bulk-suspend ids piped from a filtered listing')
|
|
31
31
|
.showHelpOnFail(false),
|
|
32
32
|
handler: withAuthHints(async (args) => {
|
|
33
33
|
const argv_ids = args.user_ids ?? [];
|
|
@@ -35,21 +35,14 @@ const users_suspend = {
|
|
|
35
35
|
? await read_stdin()
|
|
36
36
|
: null;
|
|
37
37
|
const ids = parse_user_ids(argv_ids, stdin);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
active: 'yes',
|
|
47
|
-
inactive: 'no'
|
|
48
|
-
});
|
|
49
|
-
if (!confirm) {
|
|
50
|
-
log.info('Cancelled.');
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
38
|
+
const preview = ids.slice(0, 5).join(', ') + (ids.length > 5 ? ', …' : '');
|
|
39
|
+
const go = await confirm({
|
|
40
|
+
message: `Suspend ${ids.length} user(s) (${preview})?`,
|
|
41
|
+
yes: args.yes
|
|
42
|
+
});
|
|
43
|
+
if (!go) {
|
|
44
|
+
log.info('Cancelled.');
|
|
45
|
+
return;
|
|
53
46
|
}
|
|
54
47
|
const api = await requireAuthAdmin(args);
|
|
55
48
|
const results = [];
|