@faable/faable 4.5.1 โ†’ 4.5.3

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.
@@ -88,9 +88,14 @@ class FaableApi {
88
88
  // Uniform handling for an expired/invalid session across every
89
89
  // command, regardless of which endpoint returned the 401.
90
90
  if (res.status === 401) {
91
- const expired = new Error('Your Faable session has expired or is invalid. Run `faable login` to sign in again.', { cause: error });
91
+ // With an API key there is no session to renew: the key itself
92
+ // is wrong or was revoked (the hosted MCP server runs on keys).
93
+ const by_key = !!process.env.FAABLE_API_KEY && !process.env.FAABLE_TOKEN;
94
+ const expired = new Error(by_key
95
+ ? 'This Faable API key is invalid or was revoked. Create a new one in the dashboard (project settings โ†’ API Keys).'
96
+ : 'Your Faable session has expired or is invalid. Run `faable login` to sign in again.', { cause: error });
92
97
  expired.status = 401;
93
- expired.code = 'session_expired';
98
+ expired.code = by_key ? 'apikey_invalid' : 'session_expired';
94
99
  throw expired;
95
100
  }
96
101
  const serverMessage = res.data?.message || res.statusText || 'Unknown Error';
@@ -288,9 +293,10 @@ class FaableApi {
288
293
  async listDomains(app_id, team) {
289
294
  return firstPage(this.listDomainsPage(app_id, team));
290
295
  }
296
+ // Without `app_id`, every domain of the project.
291
297
  async listDomainsPage(app_id, team, params = {}) {
292
298
  return data(this.client.get(`/domain`, {
293
- params: { app_id, ...page_params(params) },
299
+ params: { ...(app_id ? { app_id } : {}), ...page_params(params) },
294
300
  headers: projectHeader(team)
295
301
  }));
296
302
  }
@@ -1,23 +1,26 @@
1
1
  import { requireApi } from '../../../api/context.js';
2
2
  import { fetch_page, print_json, print, more_hint, list_options } from '../../../lib/listing.js';
3
3
  import { log } from '../../../log.js';
4
- import { resolve_app_id } from '../resolve_app_id.js';
4
+ import { requireProject } from '../../../api/project.js';
5
+ import { find_app_id } from '../resolve_app_id.js';
5
6
  import { dns_badge, cname_target } from './format.js';
6
7
 
7
8
  const domains_list = {
8
9
  command: 'list',
9
- describe: 'List custom domains of the app',
10
+ describe: 'List custom domains of the app (or of the project, outside a linked repo)',
10
11
  builder: yargs => list_options(yargs)
11
12
  .option('app', {
12
13
  alias: 'a',
13
14
  type: 'string',
14
15
  description: 'App id, name or slug (defaults to the linked app)'
15
16
  })
16
- .example('$0 deploy domains list', 'Domains of the linked app')
17
+ .example('$0 deploy domains list', 'Domains of the linked app, or of the active project when there is none')
17
18
  .showHelpOnFail(false),
18
19
  handler: async (args) => {
19
20
  const ctx = await requireApi();
20
- const app_id = await resolve_app_id(args.app, ctx.appId, ctx.api);
21
+ const app_id = await find_app_id(args.app, ctx.appId, ctx.api);
22
+ if (!app_id)
23
+ return list_project_domains(ctx.api, args);
21
24
  const app = await ctx.api.getApp(app_id);
22
25
  const page = await fetch_page(p => ctx.api.listDomainsPage(app_id, app.team, p), args);
23
26
  if (args.json)
@@ -48,5 +51,35 @@ const domains_list = {
48
51
  }
49
52
  }
50
53
  };
54
+ // No app here (an unlinked directory): the domains of the active project,
55
+ // each with the app it routes to.
56
+ const list_project_domains = async (api, args) => {
57
+ const { id: project } = await requireProject(api);
58
+ const page = await fetch_page(p => api.listDomainsPage(undefined, project, p), args);
59
+ if (args.json)
60
+ return print_json(page);
61
+ const domains = page.data;
62
+ if (domains.length === 0) {
63
+ log.info(`๐ŸŒ No custom domains in ${project}.`);
64
+ log.info(`Add one with: faable deploy domains add <yourdomain.com> --app <app>.`);
65
+ return;
66
+ }
67
+ const apps = await fetch_page(p => api.listApps(p, project), {
68
+ limit: 200,
69
+ all: true
70
+ });
71
+ const app_name = new Map(apps.data.map(a => [a.id, a.name]));
72
+ const label = (d) => d.app_id ? app_name.get(d.app_id) ?? d.app_id : '(no app)';
73
+ log.info(`๐ŸŒ ${domains.length} domain(s) in ${project}:`);
74
+ const width = Math.max(...domains.map(d => d.fqdn.length));
75
+ const app_width = Math.max(...domains.map(d => label(d).length));
76
+ for (const domain of domains) {
77
+ const tls = domain.tls ? 'tls' : 'no-tls';
78
+ print(` ${domain.fqdn.padEnd(width)} ${label(domain).padEnd(app_width)} ${dns_badge(domain)} (${tls})`);
79
+ }
80
+ const hint = more_hint(page);
81
+ if (hint)
82
+ log.info(hint);
83
+ };
51
84
 
52
85
  export { domains_list };
@@ -23,7 +23,6 @@ class CredentialsStore {
23
23
  await fs.ensureDir(this.faable_home);
24
24
  await fs.writeJSON(this.credentials_path, config, { spaces: 2 });
25
25
  await fs.chmod(this.credentials_path, 0o600);
26
- this.log.info(`Stored credentials`);
27
26
  }
28
27
  async loadCredentials() {
29
28
  const old_path = path__default.join(this.faable_home, 'credentials');
package/dist/mcp/http.js CHANGED
@@ -15,14 +15,14 @@ import { TOOLS } from './tools.js';
15
15
  // Streamable HTTP, stateless: every POST /mcp builds a server and a transport
16
16
  // for that one request, so any instance can answer anything. The caller
17
17
  // authenticates with a Faable API key (dashboard โ†’ project settings โ†’ API
18
- // keys). โš ๏ธ A key acts with its OWNER's access, not only on the project it was
19
- // created in (arch/deploy/mcp-cli-gaps.md) โ€” /mcp/<project> is how a caller
20
- // narrows it. Each tool call runs the CLI as a child
18
+ // keys). A key belongs to the project it was created in, and the api holds it
19
+ // there (api 2.199.2, backlog ยง302): it never acts as its owner nor reaches
20
+ // another project. Each tool call runs the CLI as a child
21
21
  // with THAT key in its environment and an empty HOME of its own โ€” two
22
22
  // callers' credentials never meet in one process, and the key is never
23
23
  // logged.
24
24
  //
25
- // POST /mcp everything the key's owner can see
25
+ // POST /mcp the key's project
26
26
  // POST /mcp/<project> pinned to one project (id, name or slug)
27
27
  // ?mode=write also the reversible writes
28
28
  // ?readonly=1 reads only โ€” not even deploy_app
@@ -151,7 +151,7 @@ const llms_txt = (base = PUBLIC_URL) => [
151
151
  '',
152
152
  '> Connects Claude, Cursor and any MCP client to Faable Deploy: read apps, deployments, build and runtime logs, traffic and domains, and deploy โ€” without leaving the editor.',
153
153
  '',
154
- `Endpoint: ${base}/mcp (Streamable HTTP). Authenticate with a Faable API key: \`Authorization: Bearer <key>\` (create one in the dashboard, project settings โ†’ API keys). The key acts with its owner's access; connect to \`${base}/mcp/<project>\` to keep every call in one project.`,
154
+ `Endpoint: ${base}/mcp (Streamable HTTP). Authenticate with a Faable API key: \`Authorization: Bearer <key>\` (create one in the dashboard, project settings โ†’ API keys). The key belongs to one project and only acts there.`,
155
155
  '',
156
156
  `- \`${base}/mcp/<project>\` pins every call to one project.`,
157
157
  '- `?mode=write` adds the reversible writes (create apps, set secrets, add domains, retry or cancel builds, change deploy settings). Nothing destructive is exposed.',
@@ -32,6 +32,7 @@ const NEXT_STEP = {
32
32
  session_expired: 'The Faable session expired. Ask the user to run `faable login` in a terminal, then retry.',
33
33
  account_suspended: 'This Faable account is suspended; the user should contact support@faable.com.',
34
34
  apikey_session: 'The user is logged in with an API key; this needs `faable login` without --apikey.',
35
+ apikey_invalid: 'The API key this server was given is invalid or was revoked. The user needs a new one from the dashboard (project settings โ†’ API Keys).',
35
36
  not_found: 'Check the id or name โ€” list_apps / list_deployments show what exists.',
36
37
  forbidden: 'This session has no access there โ€” check the project (list_projects).',
37
38
  app_required: 'Pass `app` (id, name or slug).',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faable/faable",
3
- "version": "4.5.1",
3
+ "version": "4.5.3",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
6
  "author": "Marc Pomar <marc@faable.com>",