From ab3cf442059c72e7d77a3ef488ad2ee3577ecc89 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 14:10:52 +0000 Subject: [PATCH 1/2] fix(link): link --app-id with a workspace API key without listing apps `link --app-id` confirmed the ID by listing apps, a platform route that refuses a workspace key, so a CI `base44 link --app-id` with BASE44_API_KEY failed with `Error listing projects: Unauthorized`. With a workspace key it now links directly; the key is checked against the app on its first app request. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tmb4PCrrV2SLRpBjkGAgXJ --- CHANGELOG.md | 1 + packages/cli/src/cli/commands/project/link.ts | 63 ++++++++++--------- packages/cli/tests/cli/link.spec.ts | 21 +++++++ 3 files changed, 57 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31fb8eecf..389db5765 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ ### Fixed +- `base44 link --app-id` works with a workspace API key (`BASE44_API_KEY`). It used to list apps to confirm the ID, which a workspace key is refused; now it links directly, and the key is checked against the app on the first request that addresses it. - `base44 deploy --git-hash` (and `site deploy --publish`) no longer fails after a successful publish when the app's URL can't be looked up, as happened with a workspace API key (`BASE44_API_KEY`). It reports the deploy and leaves out the URL. - An app id passed with `--app-id` or `BASE44_APP_ID` no longer discards the project the command runs in. Commands that need a project — `site dev`, `dev`, `connectors pull`/`push` — refused as if there were none. A checkout with `base44/config.jsonc` but no `.app.jsonc`, such as a hosted sandbox, can now run them. - The `backend-and-client` template names its entity file `Task.jsonc`, matching the entity's `name`; the Base44 platform derives the entity from the filename, so `task.jsonc` read as a second entity. diff --git a/packages/cli/src/cli/commands/project/link.ts b/packages/cli/src/cli/commands/project/link.ts index cb31e920a..6b7619633 100644 --- a/packages/cli/src/cli/commands/project/link.ts +++ b/packages/cli/src/cli/commands/project/link.ts @@ -9,6 +9,7 @@ import { onPromptCancel, theme, } from "@/cli/utils/index.js"; +import { hasWorkspaceApiKeyAuth } from "@/core/auth/config.js"; import { ConfigExistsError, ConfigNotFoundError, @@ -179,38 +180,44 @@ async function link( : await promptForLinkAction(); if (action === "choose") { - const projects = await runTask( - "Fetching projects...", - async () => listProjects(), - { - successMessage: "Projects fetched", - errorMessage: "Failed to fetch projects", - }, - ); - - if (!projects.length) { - return { outroMessage: "No projects available for linking" }; - } - let linkedAppId: string; - if (appId) { - const project = projects.find((p) => p.id === appId); - if (!project) { - throw new InvalidInputError(`App with ID "${appId}" not found.`, { - hints: [ - { message: "Check the app ID is correct" }, - { - message: - "Use 'base44 link' without --app-id to see available projects", - }, - ], - }); - } + // A workspace API key can't list apps; it is checked against this app on + // the first request that addresses it. + if (appId && hasWorkspaceApiKeyAuth()) { linkedAppId = appId; } else { - const selectedProject = await promptForExistingProject(projects); - linkedAppId = selectedProject.id; + const projects = await runTask( + "Fetching projects...", + async () => listProjects(), + { + successMessage: "Projects fetched", + errorMessage: "Failed to fetch projects", + }, + ); + + if (!projects.length) { + return { outroMessage: "No projects available for linking" }; + } + + if (appId) { + const project = projects.find((p) => p.id === appId); + if (!project) { + throw new InvalidInputError(`App with ID "${appId}" not found.`, { + hints: [ + { message: "Check the app ID is correct" }, + { + message: + "Use 'base44 link' without --app-id to see available projects", + }, + ], + }); + } + linkedAppId = appId; + } else { + const selectedProject = await promptForExistingProject(projects); + linkedAppId = selectedProject.id; + } } await runTask( diff --git a/packages/cli/tests/cli/link.spec.ts b/packages/cli/tests/cli/link.spec.ts index ca6be2e26..1db92b850 100644 --- a/packages/cli/tests/cli/link.spec.ts +++ b/packages/cli/tests/cli/link.spec.ts @@ -65,6 +65,27 @@ describe("link command", () => { expect(appConfig).toContain("existing-app-id"); }); + it("links --app-id with a workspace API key, which can't list apps", async () => { + await t.givenProject(fixture("no-app-config")); + t.givenEnv({ + BASE44_API_KEY: + "b44k_dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + }); + t.api.mockError("get", "/api/apps", { + status: 401, + body: { detail: "Unauthorized. ['api_key'] -> Object not found" }, + }); + + const result = await t.run("link", "--app-id", "existing-app-id"); + + t.expectResult(result).toSucceed(); + const appConfig = await readFile( + join(t.getTempDir(), "project", "base44", ".app.jsonc"), + "utf-8", + ); + expect(appConfig).toContain("existing-app-id"); + }); + it("links an editor-created app (managed source code)", async () => { await t.givenLoggedInWithProject(fixture("no-app-config")); t.api.mockListProjects([ From 3c627712eb99032e7727ea662e8e4ea6aa82fb83 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 14:14:57 +0000 Subject: [PATCH 2/2] fix(link): never list apps when --app-id is given The listing only confirmed an explicit id, covered just the 50 most recently updated apps, and is refused to a workspace key. The first request that addresses the app checks the id anyway. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tmb4PCrrV2SLRpBjkGAgXJ --- CHANGELOG.md | 2 +- packages/cli/src/cli/commands/project/link.ts | 31 +++---------------- packages/cli/tests/cli/link.spec.ts | 30 +----------------- 3 files changed, 7 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 389db5765..f523079c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ ### Fixed -- `base44 link --app-id` works with a workspace API key (`BASE44_API_KEY`). It used to list apps to confirm the ID, which a workspace key is refused; now it links directly, and the key is checked against the app on the first request that addresses it. +- `base44 link --app-id` links directly instead of first listing your apps to confirm the ID. The listing only covered the 50 most recently updated apps, and a workspace API key (`BASE44_API_KEY`) is refused on it, so `link --app-id` failed in CI. A wrong ID now fails on the first request that addresses the app. - `base44 deploy --git-hash` (and `site deploy --publish`) no longer fails after a successful publish when the app's URL can't be looked up, as happened with a workspace API key (`BASE44_API_KEY`). It reports the deploy and leaves out the URL. - An app id passed with `--app-id` or `BASE44_APP_ID` no longer discards the project the command runs in. Commands that need a project — `site dev`, `dev`, `connectors pull`/`push` — refused as if there were none. A checkout with `base44/config.jsonc` but no `.app.jsonc`, such as a hosted sandbox, can now run them. - The `backend-and-client` template names its entity file `Task.jsonc`, matching the entity's `name`; the Base44 platform derives the entity from the filename, so `task.jsonc` read as a second entity. diff --git a/packages/cli/src/cli/commands/project/link.ts b/packages/cli/src/cli/commands/project/link.ts index 6b7619633..3837be368 100644 --- a/packages/cli/src/cli/commands/project/link.ts +++ b/packages/cli/src/cli/commands/project/link.ts @@ -9,7 +9,6 @@ import { onPromptCancel, theme, } from "@/cli/utils/index.js"; -import { hasWorkspaceApiKeyAuth } from "@/core/auth/config.js"; import { ConfigExistsError, ConfigNotFoundError, @@ -180,13 +179,10 @@ async function link( : await promptForLinkAction(); if (action === "choose") { - let linkedAppId: string; - - // A workspace API key can't list apps; it is checked against this app on - // the first request that addresses it. - if (appId && hasWorkspaceApiKeyAuth()) { - linkedAppId = appId; - } else { + // An explicit id needs no listing: the first request that addresses the app + // checks it, and a workspace API key can't list apps at all. + let linkedAppId = appId; + if (!linkedAppId) { const projects = await runTask( "Fetching projects...", async () => listProjects(), @@ -200,24 +196,7 @@ async function link( return { outroMessage: "No projects available for linking" }; } - if (appId) { - const project = projects.find((p) => p.id === appId); - if (!project) { - throw new InvalidInputError(`App with ID "${appId}" not found.`, { - hints: [ - { message: "Check the app ID is correct" }, - { - message: - "Use 'base44 link' without --app-id to see available projects", - }, - ], - }); - } - linkedAppId = appId; - } else { - const selectedProject = await promptForExistingProject(projects); - linkedAppId = selectedProject.id; - } + linkedAppId = (await promptForExistingProject(projects)).id; } await runTask( diff --git a/packages/cli/tests/cli/link.spec.ts b/packages/cli/tests/cli/link.spec.ts index 1db92b850..a3825f696 100644 --- a/packages/cli/tests/cli/link.spec.ts +++ b/packages/cli/tests/cli/link.spec.ts @@ -44,13 +44,6 @@ describe("link command", () => { it("links an existing app with --app-id", async () => { await t.givenLoggedInWithProject(fixture("no-app-config")); - t.api.mockListProjects([ - { - id: "existing-app-id", - name: "Existing App", - is_managed_source_code: false, - }, - ]); const result = await t.run("link", "--app-id", "existing-app-id"); @@ -65,7 +58,7 @@ describe("link command", () => { expect(appConfig).toContain("existing-app-id"); }); - it("links --app-id with a workspace API key, which can't list apps", async () => { + it("links --app-id without listing apps, which a workspace API key can't do", async () => { await t.givenProject(fixture("no-app-config")); t.givenEnv({ BASE44_API_KEY: @@ -88,13 +81,6 @@ describe("link command", () => { it("links an editor-created app (managed source code)", async () => { await t.givenLoggedInWithProject(fixture("no-app-config")); - t.api.mockListProjects([ - { - id: "editor-app-id", - name: "Editor App", - is_managed_source_code: true, - }, - ]); const result = await t.run("link", "--app-id", "editor-app-id"); @@ -105,13 +91,6 @@ describe("link command", () => { it("links an existing app with legacy --project-id", async () => { await t.givenLoggedInWithProject(fixture("no-app-config")); - t.api.mockListProjects([ - { - id: "legacy-app-id", - name: "Legacy App", - is_managed_source_code: false, - }, - ]); const result = await t.run("link", "--project-id", "legacy-app-id"); @@ -122,13 +101,6 @@ describe("link command", () => { it("links an existing app with legacy --projectId", async () => { await t.givenLoggedInWithProject(fixture("no-app-config")); - t.api.mockListProjects([ - { - id: "camel-legacy-app-id", - name: "Camel Legacy App", - is_managed_source_code: false, - }, - ]); const result = await t.run("link", "--projectId", "camel-legacy-app-id");