@faable/faable 3.4.1 → 4.0.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/dist/api/FaableApi.js +49 -9
- package/dist/api/auth_admin.js +144 -35
- package/dist/api/context.js +9 -0
- package/dist/api/project.js +62 -0
- package/dist/commands/auth/accounts/index.js +76 -0
- package/dist/commands/auth/actions/create.js +2 -2
- package/dist/commands/auth/actions/get.js +3 -2
- package/dist/commands/auth/actions/list.js +13 -13
- package/dist/commands/auth/actions/update.js +2 -2
- package/dist/commands/auth/clients/create.js +2 -2
- package/dist/commands/auth/clients/get.js +2 -2
- package/dist/commands/auth/clients/list.js +13 -13
- package/dist/commands/auth/index.js +6 -3
- package/dist/commands/auth/logs/get.js +3 -2
- package/dist/commands/auth/logs/list.js +13 -13
- package/dist/commands/auth/options.js +5 -28
- package/dist/commands/auth/render.js +2 -14
- package/dist/commands/auth/users/get.js +5 -5
- package/dist/commands/auth/users/import.js +9 -2
- package/dist/commands/auth/users/list.js +13 -13
- package/dist/commands/auth/users/reinstate.js +2 -2
- package/dist/commands/auth/users/suspend.js +2 -2
- package/dist/commands/deploy/domains/list.js +10 -3
- package/dist/commands/deploy/inspect/deployments.js +12 -13
- package/dist/commands/deploy/inspect/list.js +18 -9
- package/dist/commands/deploy/secrets/list.js +11 -2
- package/dist/commands/deploy/waf/list.js +6 -2
- package/dist/commands/project/index.js +95 -0
- package/dist/commands/whoami/index.js +55 -5
- package/dist/index.js +28 -2
- package/dist/lib/ContextStore.js +30 -0
- package/dist/lib/listing.js +91 -0
- package/dist/log.js +12 -1
- package/package.json +1 -1
- package/dist/commands/auth/paging.js +0 -9
package/dist/api/FaableApi.js
CHANGED
|
@@ -33,6 +33,11 @@ const allPages = async (fetch_page) => {
|
|
|
33
33
|
} while (next);
|
|
34
34
|
return items;
|
|
35
35
|
};
|
|
36
|
+
// Only what's set: the list endpoints answer 400 to an unknown or empty param.
|
|
37
|
+
const page_params = ({ pageSize, next }) => ({
|
|
38
|
+
...(pageSize ? { pageSize } : {}),
|
|
39
|
+
...(next ? { next } : {})
|
|
40
|
+
});
|
|
36
41
|
const data = async (res) => {
|
|
37
42
|
const items = (await res).data;
|
|
38
43
|
return items;
|
|
@@ -40,6 +45,10 @@ const data = async (res) => {
|
|
|
40
45
|
class FaableApi {
|
|
41
46
|
client;
|
|
42
47
|
strategy;
|
|
48
|
+
// The active project (`--project`, FAABLE_PROJECT, `faable project use`),
|
|
49
|
+
// when there is one. Project-wide listings are scoped to it; calls about
|
|
50
|
+
// one app keep using that app's own project.
|
|
51
|
+
project;
|
|
43
52
|
constructor(config) {
|
|
44
53
|
const { authStrategy, auth } = config;
|
|
45
54
|
this.client = create_base_client();
|
|
@@ -110,10 +119,35 @@ class FaableApi {
|
|
|
110
119
|
async issueAuthAccountToken(account_id) {
|
|
111
120
|
return data(this.client.post(`/auth-accounts/${account_id}/token`));
|
|
112
121
|
}
|
|
122
|
+
// Every app the caller can see, across projects. Used to match the working
|
|
123
|
+
// directory's repository to its app: the link is global, so a repo whose
|
|
124
|
+
// app lives outside the active project must still resolve.
|
|
113
125
|
async list() {
|
|
114
|
-
return allPages(next =>
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
return allPages(next => this.listApps({ pageSize: 200, next }, null));
|
|
127
|
+
}
|
|
128
|
+
// One page of apps, scoped to `project` (default: the active project).
|
|
129
|
+
// `null` = unscoped. The api filters by the header, so an admin session
|
|
130
|
+
// doesn't page through the whole platform.
|
|
131
|
+
async listApps(params, project = this.project ?? null) {
|
|
132
|
+
return data(this.client.get(`/app`, {
|
|
133
|
+
params: page_params(params),
|
|
134
|
+
...(project ? { headers: projectHeader(project) } : {})
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
// Projects the caller belongs to (an admin session sees all of them).
|
|
138
|
+
// `q` searches name/description/slug; `user_id` keeps the ones they own.
|
|
139
|
+
async listProjects(params) {
|
|
140
|
+
const { q, user_id, ...page } = params;
|
|
141
|
+
return data(this.client.get(`/project`, {
|
|
142
|
+
params: { ...page_params(page), ...(q ? { q } : {}), ...(user_id ? { user_id } : {}) }
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
async getProject(project_id) {
|
|
146
|
+
return data(this.client.get(`/project/${project_id}`));
|
|
147
|
+
}
|
|
148
|
+
// The Faable Auth tenants of a project. Membership-checked by the api.
|
|
149
|
+
async listProjectAuthAccounts(project_id, params = {}) {
|
|
150
|
+
return data(this.client.get(`/project/${project_id}/auth-accounts`, { params: page_params(params) }));
|
|
117
151
|
}
|
|
118
152
|
async getBySlug(slug) {
|
|
119
153
|
return data(this.client.get(`/app/slug/${slug}`));
|
|
@@ -209,10 +243,13 @@ class FaableApi {
|
|
|
209
243
|
// Deployments of an app, newest first (the API's list index sorts
|
|
210
244
|
// createdAt desc). Team pinned via header — same reason as domains.
|
|
211
245
|
async listDeployments(app_id, team) {
|
|
212
|
-
return firstPage(
|
|
213
|
-
|
|
246
|
+
return firstPage(this.listDeploymentsPage(app_id, team));
|
|
247
|
+
}
|
|
248
|
+
async listDeploymentsPage(app_id, team, params = {}) {
|
|
249
|
+
return data(this.client.get(`/deployment`, {
|
|
250
|
+
params: { app_id, ...page_params(params) },
|
|
214
251
|
headers: projectHeader(team)
|
|
215
|
-
}))
|
|
252
|
+
}));
|
|
216
253
|
}
|
|
217
254
|
// Build and deploy the current head of the deploy branch server-side —
|
|
218
255
|
// the same path a push webhook takes, same-commit dedupe included.
|
|
@@ -236,10 +273,13 @@ class FaableApi {
|
|
|
236
273
|
// so every call pins the app's project via `x-faable-project` (same pattern
|
|
237
274
|
// as createSecretsBatch).
|
|
238
275
|
async listDomains(app_id, team) {
|
|
239
|
-
return firstPage(
|
|
240
|
-
|
|
276
|
+
return firstPage(this.listDomainsPage(app_id, team));
|
|
277
|
+
}
|
|
278
|
+
async listDomainsPage(app_id, team, params = {}) {
|
|
279
|
+
return data(this.client.get(`/domain`, {
|
|
280
|
+
params: { app_id, ...page_params(params) },
|
|
241
281
|
headers: projectHeader(team)
|
|
242
|
-
}))
|
|
282
|
+
}));
|
|
243
283
|
}
|
|
244
284
|
async createDomain(team, params) {
|
|
245
285
|
return data(this.client.post(`/domain`, params, {
|
package/dist/api/auth_admin.js
CHANGED
|
@@ -1,48 +1,157 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path__default from 'path';
|
|
4
|
+
import { ContextStore } from '../lib/ContextStore.js';
|
|
1
5
|
import { CredentialsStore } from '../lib/CredentialsStore.js';
|
|
2
6
|
import { log } from '../log.js';
|
|
3
7
|
import { FaableApi } from './FaableApi.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
8
|
+
import { AUTH_DOMAIN, createAnonymousAuthApi, createBearerAuthApi } from './auth.js';
|
|
9
|
+
import { requireProject } from './project.js';
|
|
10
|
+
import { loadLiveCredentials, isTokenLive } from './session.js';
|
|
6
11
|
import { bearer_strategy } from './strategies/bearer.strategy.js';
|
|
7
12
|
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// auth/docs/management-api-m2m-token.md for the M2M path).
|
|
18
|
-
const requireAuthAdmin = async (opts = {}) => {
|
|
19
|
-
let token = process.env.FAABLE_TOKEN;
|
|
20
|
-
if (!token) {
|
|
21
|
-
const store = new CredentialsStore();
|
|
22
|
-
const config = await loadLiveCredentials(store);
|
|
23
|
-
if (config?.apikey && !config.token) {
|
|
24
|
-
// Deploy API keys are not Auth management credentials.
|
|
25
|
-
log.error("❌ You are logged in with an API key, but `faable auth` needs a browser session. Run 'faable login' (without --apikey) first.");
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
token = config?.token;
|
|
13
|
+
// The session bearer: FAABLE_TOKEN, else `faable login` (auto-refreshed).
|
|
14
|
+
const requireSessionToken = async () => {
|
|
15
|
+
if (process.env.FAABLE_TOKEN)
|
|
16
|
+
return process.env.FAABLE_TOKEN;
|
|
17
|
+
const config = await loadLiveCredentials(new CredentialsStore());
|
|
18
|
+
if (config?.apikey && !config.token) {
|
|
19
|
+
// Deploy API keys are not Auth management credentials.
|
|
20
|
+
log.error("❌ You are logged in with an API key, but `faable auth` needs a browser session. Run 'faable login' (without --apikey) first.");
|
|
21
|
+
process.exit(1);
|
|
29
22
|
}
|
|
30
|
-
if (!token) {
|
|
23
|
+
if (!config?.token) {
|
|
31
24
|
log.error("❌ Not logged in. Run 'faable login' first.");
|
|
32
25
|
process.exit(1);
|
|
33
26
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
27
|
+
return config.token;
|
|
28
|
+
};
|
|
29
|
+
// Which Auth tenant `faable auth` manages, in precedence order:
|
|
30
|
+
//
|
|
31
|
+
// 1. --account / FAABLE_AUTH_ACCOUNT — an explicit tenant id;
|
|
32
|
+
// 2. --auth-url / FAABLE_AUTH_URL — a tenant host, looked up;
|
|
33
|
+
// 3. the active project's tenant — GET /project/:id/auth-accounts. One
|
|
34
|
+
// tenant: that one. Several: the one `faable auth use` stored for the
|
|
35
|
+
// project, or an error listing them.
|
|
36
|
+
//
|
|
37
|
+
// There is no default host any more: it used to be the Faable tenant, so a
|
|
38
|
+
// customer who forgot --auth-url was listing (or suspending) OUR users.
|
|
39
|
+
const resolveTenant = async (opts, api, store = new ContextStore(), env = process.env) => {
|
|
40
|
+
const account = opts.account || env.FAABLE_AUTH_ACCOUNT;
|
|
41
|
+
const authUrl = opts.authUrl || env.FAABLE_AUTH_URL;
|
|
42
|
+
if (account) {
|
|
43
|
+
return { account, domain: authUrl || AUTH_DOMAIN, source: 'account' };
|
|
44
|
+
}
|
|
45
|
+
if (authUrl) {
|
|
46
|
+
return {
|
|
47
|
+
account: await resolveAccountByHost(authUrl),
|
|
48
|
+
domain: authUrl,
|
|
49
|
+
source: 'auth-url'
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const project = await requireProject(api, store, env);
|
|
53
|
+
const accounts = (await api.listProjectAuthAccounts(project.id, { pageSize: 200 }))
|
|
54
|
+
.results;
|
|
55
|
+
const chosen = await chooseAccount(project.id, accounts, store);
|
|
56
|
+
return {
|
|
57
|
+
account: chosen.id,
|
|
58
|
+
domain: `https://${chosen.domain}`,
|
|
59
|
+
source: 'project',
|
|
60
|
+
project: project.id,
|
|
61
|
+
name: chosen.name
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
const chooseAccount = async (project, accounts, store = new ContextStore()) => {
|
|
65
|
+
if (accounts.length === 1)
|
|
66
|
+
return accounts[0];
|
|
67
|
+
if (accounts.length === 0) {
|
|
68
|
+
throw new Error(`Project ${project} has no Faable Auth tenant. Create one in the dashboard (https://dashboard.faable.com), or target one with --account <account_id>.`);
|
|
69
|
+
}
|
|
70
|
+
const stored = (await store.load()).auth_accounts?.[project];
|
|
71
|
+
const match = accounts.find(a => a.id === stored);
|
|
72
|
+
if (match)
|
|
73
|
+
return match;
|
|
74
|
+
const list = accounts.map(a => `${a.name} (${a.id})`).join(', ');
|
|
75
|
+
throw new Error(`Project ${project} has ${accounts.length} Auth tenants: ${list}. Pick one with: faable auth use <account_id>, or pass --account <account_id>.`);
|
|
76
|
+
};
|
|
77
|
+
// A management-API client for the tenant `resolveTenant` picks, with the
|
|
78
|
+
// per-tenant token the deploy api issues (cached until it expires); the
|
|
79
|
+
// session bearer is the fallback where no such token exists.
|
|
80
|
+
const requireAuthAdmin = async (opts = {}) => {
|
|
81
|
+
const token = await requireSessionToken();
|
|
82
|
+
const deploy = FaableApi.create({
|
|
83
|
+
authStrategy: bearer_strategy,
|
|
84
|
+
auth: { token }
|
|
85
|
+
});
|
|
86
|
+
const tenant = await resolveTenant(opts, deploy);
|
|
87
|
+
const tenantToken = tenant.account
|
|
88
|
+
? await cachedTenantToken(token, tenant.account, deploy)
|
|
89
|
+
: undefined;
|
|
41
90
|
return createBearerAuthApi(tenantToken ?? token, {
|
|
42
|
-
domain,
|
|
43
|
-
account: tenant
|
|
91
|
+
domain: tenant.domain,
|
|
92
|
+
account: tenant.account
|
|
44
93
|
});
|
|
45
94
|
};
|
|
95
|
+
// Every `faable auth` call used to mint a fresh tenant token — one extra
|
|
96
|
+
// round trip per command, and a burst of them from a script or the MCP
|
|
97
|
+
// server. It lives for minutes (`expires_in`), so it's kept next to the
|
|
98
|
+
// credentials (0600) and reused while it's live, for the same session only.
|
|
99
|
+
const cachedTenantToken = async (session_token, account_id, api, cache = new TenantTokenCache()) => {
|
|
100
|
+
const sub = subject(session_token);
|
|
101
|
+
const hit = await cache.get(account_id, sub);
|
|
102
|
+
if (hit)
|
|
103
|
+
return hit;
|
|
104
|
+
const issued = await issueTenantToken(session_token, account_id, api);
|
|
105
|
+
if (issued)
|
|
106
|
+
await cache.set(account_id, sub, issued);
|
|
107
|
+
return issued;
|
|
108
|
+
};
|
|
109
|
+
const subject = (token) => {
|
|
110
|
+
try {
|
|
111
|
+
const claims = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString());
|
|
112
|
+
return typeof claims.sub === 'string' ? claims.sub : undefined;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
class TenantTokenCache {
|
|
119
|
+
file;
|
|
120
|
+
constructor(file = path__default.join(os.homedir(), '.faable', 'tenant-tokens.json')) {
|
|
121
|
+
this.file = file;
|
|
122
|
+
}
|
|
123
|
+
async read() {
|
|
124
|
+
try {
|
|
125
|
+
return (await fs.readJSON(this.file)) || {};
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async get(account_id, sub) {
|
|
132
|
+
const entry = (await this.read())[account_id];
|
|
133
|
+
if (!entry || entry.sub !== sub || !isTokenLive(entry.token))
|
|
134
|
+
return;
|
|
135
|
+
return entry.token;
|
|
136
|
+
}
|
|
137
|
+
async set(account_id, sub, token) {
|
|
138
|
+
try {
|
|
139
|
+
const all = await this.read();
|
|
140
|
+
// Drop the dead ones while we're here.
|
|
141
|
+
for (const [id, entry] of Object.entries(all)) {
|
|
142
|
+
if (!isTokenLive(entry.token))
|
|
143
|
+
delete all[id];
|
|
144
|
+
}
|
|
145
|
+
all[account_id] = { token, sub };
|
|
146
|
+
await fs.ensureDir(path__default.dirname(this.file));
|
|
147
|
+
await fs.writeJSON(this.file, all, { spaces: 2, mode: 0o600 });
|
|
148
|
+
await fs.chmod(this.file, 0o600);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// A cache that can't be written is a cache miss next time, not an error.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
46
155
|
// The tenant behind an Auth host (`<slug>.auth.faable.link` or a custom
|
|
47
156
|
// domain), through the anonymous public lookup. `undefined` when it can't be
|
|
48
157
|
// resolved: the server then picks the tenant from the host, as before.
|
|
@@ -87,7 +196,7 @@ const hintAuthError = (e) => {
|
|
|
87
196
|
throw new Error("Unauthorized (401) by the Auth management API. Your session may have expired — run 'faable login' and retry.");
|
|
88
197
|
}
|
|
89
198
|
if (status === 403) {
|
|
90
|
-
throw new Error('Forbidden (403): your session is not allowed to manage this tenant. Check --
|
|
199
|
+
throw new Error('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.');
|
|
91
200
|
}
|
|
92
201
|
throw e;
|
|
93
202
|
};
|
|
@@ -103,4 +212,4 @@ const withAuthHints = (handler) => {
|
|
|
103
212
|
};
|
|
104
213
|
};
|
|
105
214
|
|
|
106
|
-
export { hintAuthError, issueTenantToken, requireAuthAdmin, resolveAccountByHost, withAuthHints };
|
|
215
|
+
export { TenantTokenCache, cachedTenantToken, chooseAccount, hintAuthError, issueTenantToken, requireAuthAdmin, requireSessionToken, resolveAccountByHost, resolveTenant, subject, withAuthHints };
|
package/dist/api/context.js
CHANGED
|
@@ -6,6 +6,7 @@ import { bearer_strategy } from './strategies/bearer.strategy.js';
|
|
|
6
6
|
import { CredentialsStore } from '../lib/CredentialsStore.js';
|
|
7
7
|
import { loadLiveCredentials } from './session.js';
|
|
8
8
|
import { log } from '../log.js';
|
|
9
|
+
import { configuredProject, resolveProjectRef } from './project.js';
|
|
9
10
|
|
|
10
11
|
const context = async (targetAppId) => {
|
|
11
12
|
let api;
|
|
@@ -49,6 +50,14 @@ const context = async (targetAppId) => {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
}
|
|
53
|
+
// The active project scopes project-wide listings (`deploy list`). Only
|
|
54
|
+
// when one is configured: picking a default here would cost a request on
|
|
55
|
+
// every command, including a deploy.
|
|
56
|
+
if (api) {
|
|
57
|
+
const configured = await configuredProject();
|
|
58
|
+
if (configured)
|
|
59
|
+
api.project = await resolveProjectRef(api, configured.ref);
|
|
60
|
+
}
|
|
52
61
|
const appId = await api?.strategy?.app_id?.();
|
|
53
62
|
return {
|
|
54
63
|
api,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { ContextStore } from '../lib/ContextStore.js';
|
|
2
|
+
|
|
3
|
+
let flag_value;
|
|
4
|
+
// Set by the global middleware in src/index.ts.
|
|
5
|
+
const setProjectFlag = (value) => {
|
|
6
|
+
flag_value = value || undefined;
|
|
7
|
+
};
|
|
8
|
+
const configuredProject = async (store = new ContextStore(), env = process.env) => {
|
|
9
|
+
if (flag_value)
|
|
10
|
+
return { ref: flag_value, source: 'flag' };
|
|
11
|
+
if (env.FAABLE_PROJECT)
|
|
12
|
+
return { ref: env.FAABLE_PROJECT, source: 'env' };
|
|
13
|
+
const { project } = await store.load();
|
|
14
|
+
if (project)
|
|
15
|
+
return { ref: project, source: 'config' };
|
|
16
|
+
return undefined;
|
|
17
|
+
};
|
|
18
|
+
const ID_RE = /^(project|team)_[a-f0-9]{24}$/;
|
|
19
|
+
// project_<hex>: the api's form. A team_<hex> (the app rows' form) is the
|
|
20
|
+
// same project.
|
|
21
|
+
const toProjectId = (id) => id.replace(/^team_/, 'project_');
|
|
22
|
+
// An id is taken as-is (the api checks membership on use); a name or slug is
|
|
23
|
+
// looked up — exactly one project must match.
|
|
24
|
+
const resolveProjectRef = async (api, ref) => {
|
|
25
|
+
if (ID_RE.test(ref))
|
|
26
|
+
return toProjectId(ref);
|
|
27
|
+
const page = await api.listProjects({ q: ref, pageSize: 50 });
|
|
28
|
+
const wanted = ref.toLowerCase();
|
|
29
|
+
const matches = page.results.filter(p => p.name.toLowerCase() === wanted || p.slug?.toLowerCase() === wanted);
|
|
30
|
+
if (matches.length === 1)
|
|
31
|
+
return matches[0].id;
|
|
32
|
+
if (matches.length === 0) {
|
|
33
|
+
throw new Error(`No project named "${ref}". See them with: faable project list`);
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`Several projects are named "${ref}": ${describe(matches)}. Use its id.`);
|
|
36
|
+
};
|
|
37
|
+
const describe = (projects) => projects.map(p => `${p.name} (${p.id})`).join(', ');
|
|
38
|
+
// The project a project-wide command needs. With none configured, a caller
|
|
39
|
+
// who belongs to exactly one project gets that one; otherwise it's an error
|
|
40
|
+
// that says how to choose.
|
|
41
|
+
const requireProject = async (api, store = new ContextStore(), env = process.env) => {
|
|
42
|
+
const configured = await configuredProject(store, env);
|
|
43
|
+
if (configured) {
|
|
44
|
+
return {
|
|
45
|
+
id: await resolveProjectRef(api, configured.ref),
|
|
46
|
+
source: configured.source
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const page = await api.listProjects({ pageSize: 2 });
|
|
50
|
+
if (page.results.length === 1 && !page.next) {
|
|
51
|
+
return { id: page.results[0].id, source: 'only' };
|
|
52
|
+
}
|
|
53
|
+
throw new Error('No project selected. Pick one with: faable project use <id|name> (list them with: faable project list), or pass --project <id|name>.');
|
|
54
|
+
};
|
|
55
|
+
const SOURCE_LABEL = {
|
|
56
|
+
flag: '--project',
|
|
57
|
+
env: 'FAABLE_PROJECT',
|
|
58
|
+
config: 'faable project use',
|
|
59
|
+
only: 'your only project'
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export { SOURCE_LABEL, configuredProject, requireProject, resolveProjectRef, setProjectFlag, toProjectId };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { requireSessionToken } from '../../../api/auth_admin.js';
|
|
2
|
+
import { FaableApi } from '../../../api/FaableApi.js';
|
|
3
|
+
import { requireProject } from '../../../api/project.js';
|
|
4
|
+
import { bearer_strategy } from '../../../api/strategies/bearer.strategy.js';
|
|
5
|
+
import { ContextStore } from '../../../lib/ContextStore.js';
|
|
6
|
+
import { fetch_page, print_json, table_lines, print, more_hint, list_options } from '../../../lib/listing.js';
|
|
7
|
+
import { log } from '../../../log.js';
|
|
8
|
+
|
|
9
|
+
const deploy_api = async () => FaableApi.create({
|
|
10
|
+
authStrategy: bearer_strategy,
|
|
11
|
+
auth: { token: await requireSessionToken() }
|
|
12
|
+
});
|
|
13
|
+
// `faable auth accounts list` — the Auth tenants of the active project, the
|
|
14
|
+
// ones `faable auth` can manage without --account.
|
|
15
|
+
const accounts_list = {
|
|
16
|
+
command: 'list',
|
|
17
|
+
describe: "List the active project's Auth tenants",
|
|
18
|
+
builder: yargs => list_options(yargs).showHelpOnFail(false),
|
|
19
|
+
handler: async (args) => {
|
|
20
|
+
const api = await deploy_api();
|
|
21
|
+
const project = await requireProject(api);
|
|
22
|
+
const page = await fetch_page(p => api.listProjectAuthAccounts(project.id, p), args);
|
|
23
|
+
if (args.json)
|
|
24
|
+
return print_json(page);
|
|
25
|
+
if (page.data.length === 0) {
|
|
26
|
+
log.info(`📭 Project ${project.id} has no Auth tenants.`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const stored = (await new ContextStore().load()).auth_accounts?.[project.id];
|
|
30
|
+
const active = page.data.length === 1 ? page.data[0].id : stored;
|
|
31
|
+
log.info(`🔐 ${page.data.length} Auth tenant(s) in ${project.id}:`);
|
|
32
|
+
for (const line of table_lines(['', 'ID', 'NAME', 'DOMAIN'], page.data.map(a => [a.id === active ? '*' : '', a.id, a.name, a.domain]))) {
|
|
33
|
+
print(line);
|
|
34
|
+
}
|
|
35
|
+
const hint = more_hint(page);
|
|
36
|
+
if (hint)
|
|
37
|
+
log.info(hint);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const accounts = {
|
|
41
|
+
command: 'accounts',
|
|
42
|
+
describe: "The active project's Auth tenants",
|
|
43
|
+
builder: yargs => yargs.command(accounts_list).demandCommand(1).showHelpOnFail(false),
|
|
44
|
+
handler: () => { }
|
|
45
|
+
};
|
|
46
|
+
// `faable auth use <account_id>` — which tenant `faable auth` manages in the
|
|
47
|
+
// active project, for a project with more than one.
|
|
48
|
+
const auth_use = {
|
|
49
|
+
command: 'use <account>',
|
|
50
|
+
describe: 'Set the Auth tenant to manage in the active project',
|
|
51
|
+
builder: yargs => yargs
|
|
52
|
+
.positional('account', { type: 'string', demandOption: true })
|
|
53
|
+
.showHelpOnFail(false),
|
|
54
|
+
handler: async (args) => {
|
|
55
|
+
const api = await deploy_api();
|
|
56
|
+
const project = await requireProject(api);
|
|
57
|
+
const { results } = await api.listProjectAuthAccounts(project.id, {
|
|
58
|
+
pageSize: 200
|
|
59
|
+
});
|
|
60
|
+
const wanted = args.account.toLowerCase();
|
|
61
|
+
const account = results.find(a => a.id === args.account ||
|
|
62
|
+
a.slug?.toLowerCase() === wanted ||
|
|
63
|
+
a.domain.toLowerCase() === wanted);
|
|
64
|
+
if (!account) {
|
|
65
|
+
const list = results.map(a => `${a.name} (${a.id})`).join(', ') || 'none';
|
|
66
|
+
throw new Error(`${args.account} is not an Auth tenant of ${project.id}. Its tenants: ${list}.`);
|
|
67
|
+
}
|
|
68
|
+
await new ContextStore().update(ctx => ({
|
|
69
|
+
...ctx,
|
|
70
|
+
auth_accounts: { ...ctx.auth_accounts, [project.id]: account.id }
|
|
71
|
+
}));
|
|
72
|
+
log.info(`✅ faable auth now manages ${account.name} (${account.domain}) in ${project.id}.`);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export { accounts, auth_use };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
|
|
3
3
|
import { log } from '../../../log.js';
|
|
4
|
-
import {
|
|
5
|
-
import { print_json } from '
|
|
4
|
+
import { tenant_options } from '../options.js';
|
|
5
|
+
import { json_option, print_json } from '../../../lib/listing.js';
|
|
6
6
|
import { formatTriggers } from './triggers.js';
|
|
7
7
|
|
|
8
8
|
const actions_create = {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
|
|
2
2
|
import { log } from '../../../log.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { tenant_options } from '../options.js';
|
|
4
|
+
import { yes_no } from '../render.js';
|
|
5
5
|
import { formatTriggers } from './triggers.js';
|
|
6
|
+
import { json_option, print_json } from '../../../lib/listing.js';
|
|
6
7
|
|
|
7
8
|
const actions_get = {
|
|
8
9
|
command: 'get <action_id>',
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
|
|
2
2
|
import { log } from '../../../log.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { list_options, fetch_page, from_paginator, print_json, table_lines, print, more_hint } from '../../../lib/listing.js';
|
|
4
|
+
import { tenant_options } from '../options.js';
|
|
5
|
+
import { yes_no, when } from '../render.js';
|
|
6
6
|
import { formatTriggers } from './triggers.js';
|
|
7
7
|
|
|
8
8
|
const actions_list = {
|
|
9
9
|
command: 'list',
|
|
10
10
|
describe: 'List actions',
|
|
11
|
-
builder: yargs =>
|
|
11
|
+
builder: yargs => list_options(tenant_options(yargs))
|
|
12
12
|
.option('query', {
|
|
13
13
|
type: 'string',
|
|
14
14
|
description: 'FaableQL filter, e.g. "enabled:true"'
|
|
@@ -16,15 +16,15 @@ const actions_list = {
|
|
|
16
16
|
.showHelpOnFail(false),
|
|
17
17
|
handler: withAuthHints(async (args) => {
|
|
18
18
|
const api = await requireAuthAdmin(args);
|
|
19
|
-
const
|
|
19
|
+
const page = await fetch_page(from_paginator(api.actionList({ query: args.query })), args);
|
|
20
20
|
if (args.json)
|
|
21
|
-
return print_json(
|
|
22
|
-
if (
|
|
21
|
+
return print_json(page);
|
|
22
|
+
if (page.data.length === 0) {
|
|
23
23
|
log.info('📭 No actions.');
|
|
24
24
|
return;
|
|
25
25
|
}
|
|
26
|
-
log.info(`⚙️ ${
|
|
27
|
-
const rows =
|
|
26
|
+
log.info(`⚙️ ${page.data.length} action(s):`);
|
|
27
|
+
const rows = page.data.map(a => [
|
|
28
28
|
a.id ?? '-',
|
|
29
29
|
a.name ?? '-',
|
|
30
30
|
formatTriggers(a),
|
|
@@ -33,11 +33,11 @@ const actions_list = {
|
|
|
33
33
|
when(a.createdAt)
|
|
34
34
|
]);
|
|
35
35
|
for (const line of table_lines(['ID', 'NAME', 'TRIGGER', 'ENABLED', 'ORDER', 'CREATED'], rows)) {
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
if (more) {
|
|
39
|
-
log.info('… more results available: raise --limit or use --all.');
|
|
36
|
+
print(line);
|
|
40
37
|
}
|
|
38
|
+
const hint = more_hint(page);
|
|
39
|
+
if (hint)
|
|
40
|
+
log.info(hint);
|
|
41
41
|
})
|
|
42
42
|
};
|
|
43
43
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
|
|
3
3
|
import { log } from '../../../log.js';
|
|
4
|
-
import {
|
|
5
|
-
import { print_json } from '
|
|
4
|
+
import { tenant_options } from '../options.js';
|
|
5
|
+
import { json_option, print_json } from '../../../lib/listing.js';
|
|
6
6
|
import { formatTriggers } from './triggers.js';
|
|
7
7
|
|
|
8
8
|
const actions_update = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
|
|
2
2
|
import { log } from '../../../log.js';
|
|
3
|
-
import {
|
|
4
|
-
import { print_json } from '
|
|
3
|
+
import { tenant_options } from '../options.js';
|
|
4
|
+
import { json_option, print_json } from '../../../lib/listing.js';
|
|
5
5
|
|
|
6
6
|
const clients_create = {
|
|
7
7
|
command: 'create',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
|
|
2
2
|
import { log } from '../../../log.js';
|
|
3
|
-
import {
|
|
4
|
-
import { print_json } from '
|
|
3
|
+
import { tenant_options } from '../options.js';
|
|
4
|
+
import { json_option, print_json } from '../../../lib/listing.js';
|
|
5
5
|
import { resolve_client } from './resolve.js';
|
|
6
6
|
|
|
7
7
|
const clients_get = {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { withAuthHints, requireAuthAdmin } from '../../../api/auth_admin.js';
|
|
2
2
|
import { log } from '../../../log.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { list_options, fetch_page, from_paginator, print_json, table_lines, print, more_hint } from '../../../lib/listing.js';
|
|
4
|
+
import { tenant_options } from '../options.js';
|
|
5
|
+
import { truncate, when } from '../render.js';
|
|
6
6
|
|
|
7
7
|
const clients_list = {
|
|
8
8
|
command: 'list',
|
|
9
9
|
describe: 'List OAuth clients',
|
|
10
|
-
builder: yargs =>
|
|
10
|
+
builder: yargs => list_options(tenant_options(yargs))
|
|
11
11
|
.option('q', {
|
|
12
12
|
type: 'string',
|
|
13
13
|
description: 'Full-text search over name/description/client_id'
|
|
@@ -15,26 +15,26 @@ const clients_list = {
|
|
|
15
15
|
.showHelpOnFail(false),
|
|
16
16
|
handler: withAuthHints(async (args) => {
|
|
17
17
|
const api = await requireAuthAdmin(args);
|
|
18
|
-
const
|
|
18
|
+
const page = await fetch_page(from_paginator(api.clientList({ q: args.q })), args);
|
|
19
19
|
if (args.json)
|
|
20
|
-
return print_json(
|
|
21
|
-
if (
|
|
20
|
+
return print_json(page);
|
|
21
|
+
if (page.data.length === 0) {
|
|
22
22
|
log.info('📭 No clients.');
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
|
-
log.info(`🔑 ${
|
|
26
|
-
const rows =
|
|
25
|
+
log.info(`🔑 ${page.data.length} client(s):`);
|
|
26
|
+
const rows = page.data.map(c => [
|
|
27
27
|
c.client_id ?? '-',
|
|
28
28
|
truncate(c.name, 28),
|
|
29
29
|
String(c.callbacks?.length ?? 0),
|
|
30
30
|
when(c.createdAt)
|
|
31
31
|
]);
|
|
32
32
|
for (const line of table_lines(['CLIENT_ID', 'NAME', 'CALLBACKS', 'CREATED'], rows)) {
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
if (more) {
|
|
36
|
-
log.info('… more results available: raise --limit or use --all.');
|
|
33
|
+
print(line);
|
|
37
34
|
}
|
|
35
|
+
const hint = more_hint(page);
|
|
36
|
+
if (hint)
|
|
37
|
+
log.info(hint);
|
|
38
38
|
})
|
|
39
39
|
};
|
|
40
40
|
|