@faable/faable 4.5.0 → 4.5.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.
@@ -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';
@@ -227,8 +232,12 @@ class FaableApi {
227
232
  async updateApp(app_id, params) {
228
233
  return data(this.client.post(`/app/${app_id}`, params));
229
234
  }
230
- async linkRepository(app_id, params) {
231
- return data(this.client.post(`/app/${app_id}/link-repository`, params));
235
+ // Team pinned via header: the api answers 400 team_required without it
236
+ // (it needs the project to check the plan and the repository guard).
237
+ async linkRepository(app_id, params, team) {
238
+ return data(this.client.post(`/app/${app_id}/link-repository`, params, {
239
+ headers: projectHeader(team)
240
+ }));
232
241
  }
233
242
  // Organizations/accounts where the Faable GitHub App is installed.
234
243
  async listGithubInstallations() {
@@ -259,11 +268,11 @@ class FaableApi {
259
268
  }
260
269
  // Build and deploy the current head of the deploy branch server-side —
261
270
  // the same path a push webhook takes, same-commit dedupe included.
262
- async deployNow(app_id) {
271
+ async deployNow(app_id, team) {
263
272
  return data(
264
273
  // deployment_id: the build this call started (api ≥ the release that
265
274
  // added it; absent from older ones).
266
- this.client.post(`/app/${app_id}/deploy`));
275
+ this.client.post(`/app/${app_id}/deploy`, undefined, { headers: projectHeader(team) }));
267
276
  }
268
277
  // Rebuild a failed deployment from its recorded source (CAS manifest or
269
278
  // git ref). The API enforces the guards: failed phase only, never older
@@ -81,7 +81,7 @@ const apps_create = {
81
81
  linked = await api.linkRepository(app.id, {
82
82
  repository,
83
83
  ...(args.branch ? { github_branch: args.branch } : {})
84
- });
84
+ }, app.team);
85
85
  }
86
86
  catch (err) {
87
87
  await api.deleteApp(app.id, app.team).catch(() => {
@@ -102,7 +102,7 @@ const apps_create = {
102
102
  let first_deploy = null;
103
103
  if (repository && args.deploy && linked.deploy_trigger === 'webhook') {
104
104
  try {
105
- const started = await api.deployNow(app.id);
105
+ const started = await api.deployNow(app.id, app.team);
106
106
  first_deploy = {
107
107
  commit: started.commit,
108
108
  branch: started.branch,
@@ -20,7 +20,7 @@ const trigger = {
20
20
  const app = await ctx.api.getApp(app_id);
21
21
  // Takes the exact same path a git push would (same-commit dedupe
22
22
  // included) — the API answers with an actionable refusal otherwise.
23
- const result = await ctx.api.deployNow(app_id);
23
+ const result = await ctx.api.deployNow(app_id, app.team);
24
24
  if (args.json)
25
25
  return print_json({ app_id, ...result });
26
26
  log.info(`🚀 Building ${result.commit.slice(0, 7)} (${result.branch}) of ${app.name} server-side${result.deployment_id ? ` as ${result.deployment_id}` : ''}.`);
@@ -77,7 +77,7 @@ const link = {
77
77
  // access to the repository before persisting the link.
78
78
  let linked;
79
79
  try {
80
- linked = await api.linkRepository(selectedApp.id, { repository: gitUrl });
80
+ linked = await api.linkRepository(selectedApp.id, { repository: gitUrl }, selectedApp.team);
81
81
  }
82
82
  catch (err) {
83
83
  const code = err?.code;
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.0",
3
+ "version": "4.5.2",
4
4
  "main": "dist/index.js",
5
5
  "license": "MIT",
6
6
  "author": "Marc Pomar <marc@faable.com>",