@faable/faable 3.4.0 → 3.4.2

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.
@@ -102,6 +102,14 @@ class FaableApi {
102
102
  static create(config = {}) {
103
103
  return new FaableApi(config);
104
104
  }
105
+ // A short-lived Management API token for ONE Faable Auth tenant, issued by
106
+ // the deploy api after checking the caller belongs to the project that owns
107
+ // it (phase 2 of arch/auth/management-api-tenant-isolation.md). `faable auth`
108
+ // manages tenants with it instead of the `faable login` token, which stops
109
+ // being platform-superadmin in phase 3.
110
+ async issueAuthAccountToken(account_id) {
111
+ return data(this.client.post(`/auth-accounts/${account_id}/token`));
112
+ }
105
113
  async list() {
106
114
  return allPages(next => data(this.client.get(`/app`, {
107
115
  params: { pageSize: 200, ...(next ? { next } : {}) }
@@ -1,7 +1,9 @@
1
1
  import { CredentialsStore } from '../lib/CredentialsStore.js';
2
2
  import { log } from '../log.js';
3
- import { createBearerAuthApi } from './auth.js';
3
+ import { FaableApi } from './FaableApi.js';
4
+ import { createBearerAuthApi, createAnonymousAuthApi } from './auth.js';
4
5
  import { loadLiveCredentials } from './session.js';
6
+ import { bearer_strategy } from './strategies/bearer.strategy.js';
5
7
 
6
8
  // Default tenant host. `faable auth` is customer-facing: a customer targets
7
9
  // their own tenant with --auth-url https://<account>.auth.faable.link (or
@@ -31,10 +33,50 @@ const requireAuthAdmin = async (opts = {}) => {
31
33
  }
32
34
  const domain = opts.authUrl || process.env.FAABLE_AUTH_URL || DEFAULT_AUTH_URL;
33
35
  const account = opts.account || process.env.FAABLE_AUTH_ACCOUNT;
34
- // The session's bearer as a strategy — with the CLI's own identity on the
35
- // wire instead of `auth-sdk` — scoped to the target tenant when one was
36
- // named. The server decides whether this token may manage that tenant.
37
- return createBearerAuthApi(token, { domain, account });
36
+ const tenant = account ?? (await resolveAccountByHost(domain));
37
+ const tenantToken = tenant ? await issueTenantToken(token, tenant) : undefined;
38
+ // Prefer the per-tenant management token; the session's bearer is the
39
+ // fallback (the platform tenant, or an api that can't issue one), exactly
40
+ // what `faable auth` sent before phase 2. Either way, scoped to the tenant.
41
+ return createBearerAuthApi(tenantToken ?? token, {
42
+ domain,
43
+ account: tenant ?? account
44
+ });
45
+ };
46
+ // The tenant behind an Auth host (`<slug>.auth.faable.link` or a custom
47
+ // domain), through the anonymous public lookup. `undefined` when it can't be
48
+ // resolved: the server then picks the tenant from the host, as before.
49
+ const resolveAccountByHost = async (domain, auth = createAnonymousAuthApi({ domain })) => {
50
+ try {
51
+ const host = new URL(domain).host;
52
+ const account = await auth.fetcher.get(`/account/host/${host}`);
53
+ return account?.id;
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ };
59
+ // Statuses that mean "no per-tenant token for this one right now" — the
60
+ // platform tenant (404 tenant_token_not_issued), an api without the endpoint,
61
+ // or auth unable to mint. Anything else (403: not a member) is a real refusal.
62
+ const FALLBACK_STATUSES = new Set([404, 501, 503]);
63
+ const issueTenantToken = async (session_token, account_id, api = FaableApi.create({
64
+ authStrategy: bearer_strategy,
65
+ auth: { token: session_token }
66
+ })) => {
67
+ try {
68
+ const { access_token } = await api.issueAuthAccountToken(account_id);
69
+ return access_token;
70
+ }
71
+ catch (e) {
72
+ const status = e?.response?.status;
73
+ if (status === 403) {
74
+ throw new Error(`Forbidden (403): you are not a member of the project that owns ${account_id}, so you can't manage it.`, { cause: e });
75
+ }
76
+ if (status === undefined || FALLBACK_STATUSES.has(status))
77
+ return undefined;
78
+ throw e;
79
+ }
38
80
  };
39
81
  // Translate raw management-API failures into actionable CLI errors. Everything
40
82
  // else is rethrown untouched (FaableApiError messages already carry
@@ -61,4 +103,4 @@ const withAuthHints = (handler) => {
61
103
  };
62
104
  };
63
105
 
64
- export { hintAuthError, requireAuthAdmin, withAuthHints };
106
+ export { hintAuthError, issueTenantToken, requireAuthAdmin, resolveAccountByHost, withAuthHints };
@@ -149,6 +149,8 @@ const users_import = {
149
149
  email: r.email,
150
150
  status: r.status,
151
151
  user_id: r.user_id,
152
+ skipped_identities: r
153
+ .skipped_identities,
152
154
  error: r.error
153
155
  });
154
156
  }
@@ -169,6 +171,11 @@ const users_import = {
169
171
  print_json({ dry_run, summary, results });
170
172
  }
171
173
  else {
174
+ const without = results.filter(r => r.skipped_identities?.length);
175
+ if (without.length) {
176
+ const names = [...new Set(without.flatMap(r => r.skipped_identities))];
177
+ log.warn(`⚠️ ${without.length} user(s) imported without their ${names.join(', ')} identity: this tenant has no connection by that name. Create it and re-run with --update-existing, or map it with --map-connection.`);
178
+ }
172
179
  for (const r of results.filter(r => r.status === 'error')) {
173
180
  log.error(`❌ record ${r.record}${r.email ? ` (${r.email})` : ''}: ${r.error?.code} — ${r.error?.message}`);
174
181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faable/faable",
3
- "version": "3.4.0",
3
+ "version": "3.4.2",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
6
  "author": "Marc Pomar <marc@faable.com>",