diff --git a/packages/cli/deno-runtime/exec.ts b/packages/cli/deno-runtime/exec.ts index 2c273aa58..6a17cd1ae 100644 --- a/packages/cli/deno-runtime/exec.ts +++ b/packages/cli/deno-runtime/exec.ts @@ -9,6 +9,8 @@ * - BASE44_APP_ID: App identifier from .app.jsonc * - BASE44_ACCESS_TOKEN: User's access token * - BASE44_APP_BASE_URL: App's published URL / subdomain (used for function calls) + * - BASE44_PRIVILEGED: When "true", adds the X-Bypass-RLS header (bypass RLS) + * - BASE44_DATA_ENV: When set, adds the X-Data-Env header (target data environment) */ export {}; @@ -17,6 +19,8 @@ const scriptPath = Deno.env.get("SCRIPT_PATH"); const appId = Deno.env.get("BASE44_APP_ID"); const accessToken = Deno.env.get("BASE44_ACCESS_TOKEN"); const appBaseUrl = Deno.env.get("BASE44_APP_BASE_URL"); +const isPrivileged = Deno.env.get("BASE44_PRIVILEGED") === "true"; +const dataEnv = Deno.env.get("BASE44_DATA_ENV"); if (!scriptPath) { console.error("SCRIPT_PATH environment variable is required"); @@ -35,10 +39,15 @@ if (!appBaseUrl) { import { createClient } from "npm:@base44/sdk"; +const customHeaders: Record = {}; +if (isPrivileged) customHeaders["X-Bypass-RLS"] = "true"; +if (dataEnv) customHeaders["X-Data-Env"] = dataEnv; + const base44 = createClient({ appId, token: accessToken, serverUrl: appBaseUrl, + headers: customHeaders, }); (globalThis as any).base44 = base44; diff --git a/packages/cli/src/cli/commands/exec.ts b/packages/cli/src/cli/commands/exec.ts index 5288d874c..f496efcdf 100644 --- a/packages/cli/src/cli/commands/exec.ts +++ b/packages/cli/src/cli/commands/exec.ts @@ -10,6 +10,8 @@ import { readAuth } from "@/core/index.js"; interface ExecOptions { local?: boolean; port?: string; + privileged?: boolean; + dataEnv?: string; } function readStdin(): Promise { @@ -91,7 +93,13 @@ async function execAction( ) : undefined; - const { exitCode } = await runScript({ appId: app!.id, code, local }); + const { exitCode } = await runScript({ + appId: app!.id, + code, + local, + privileged: options.privileged, + dataEnv: options.dataEnv, + }); if (exitCode !== 0) { process.exitCode = exitCode; @@ -113,6 +121,14 @@ export function getExecCommand(): Command { "--port ", `Port the local dev server is on (with --local; defaults to ${DEFAULT_DEV_SERVER_PORT})`, ) + .option( + "--privileged", + "Run with admin privileges (bypass RLS). Requires app owner/editor role.", + ) + .option( + "--data-env ", + "Data environment to run against (e.g. dev, prod)", + ) .addHelpText( "after", ` @@ -124,7 +140,10 @@ Examples: $ echo "const users = await base44.entities.User.list()" | base44 exec Against the local dev server (base44 dev must be running): - $ echo "await base44.entities.Task.create({ title: 'seed' })" | base44 exec --local`, + $ echo "await base44.entities.Task.create({ title: 'seed' })" | base44 exec --local + + With privileged access (bypass RLS): + $ echo "const all = await base44.entities.Task.list()" | base44 exec --privileged`, ) .action(execAction); } diff --git a/packages/cli/src/core/exec/run-script.ts b/packages/cli/src/core/exec/run-script.ts index 37d63b9fe..82473b8b9 100644 --- a/packages/cli/src/core/exec/run-script.ts +++ b/packages/cli/src/core/exec/run-script.ts @@ -14,6 +14,8 @@ interface RunScriptOptions { * rather than fetched via `getSiteUrl()` / `getAppUserToken()`. */ local?: { serverUrl: string; token: string }; + privileged?: boolean; + dataEnv?: string; } interface RunScriptResult { @@ -23,7 +25,7 @@ interface RunScriptResult { export async function runScript( options: RunScriptOptions, ): Promise { - const { appId, code, local } = options; + const { appId, code, local, privileged, dataEnv } = options; verifyDenoInstalled("to run scripts with exec"); @@ -60,6 +62,8 @@ export async function runScript( BASE44_APP_ID: appId, BASE44_ACCESS_TOKEN: appUserToken, BASE44_APP_BASE_URL: appBaseUrl, + ...(privileged ? { BASE44_PRIVILEGED: "true" } : {}), + ...(dataEnv ? { BASE44_DATA_ENV: dataEnv } : {}), }, stdio: "inherit", }, diff --git a/packages/cli/tests/cli/exec.spec.ts b/packages/cli/tests/cli/exec.spec.ts index eda1ef842..85e11421b 100644 --- a/packages/cli/tests/cli/exec.spec.ts +++ b/packages/cli/tests/cli/exec.spec.ts @@ -175,4 +175,143 @@ describe("exec command", () => { t.expectResult(result).toFail(); t.expectResult(result).toContain("--port can only be used with --local"); }); + + // ─── PRIVILEGED / DATA-ENV HEADERS (ported from #435) ───────── + + it("sends the X-Bypass-RLS header when --privileged is set", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockAuthToken("test-app-token"); + t.api.mockSiteUrl({ url: t.api.baseUrl }); + + let capturedHeaders: Record = {}; + t.api.mockRoute( + "GET", + `/api/apps/${t.api.appId}/entities/Task`, + (req, res) => { + capturedHeaders = req.headers as Record; + res.json([]); + }, + ); + t.givenStdin("await base44.entities.Task.list();"); + + const result = await t.run("exec", "--privileged"); + + t.expectResult(result).toSucceed(); + expect(capturedHeaders["x-bypass-rls"]).toBe("true"); + }); + + it("does not send the X-Bypass-RLS header without --privileged", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockAuthToken("test-app-token"); + t.api.mockSiteUrl({ url: t.api.baseUrl }); + + let capturedHeaders: Record = {}; + t.api.mockRoute( + "GET", + `/api/apps/${t.api.appId}/entities/Task`, + (req, res) => { + capturedHeaders = req.headers as Record; + res.json([]); + }, + ); + t.givenStdin("await base44.entities.Task.list();"); + + const result = await t.run("exec"); + + t.expectResult(result).toSucceed(); + expect(capturedHeaders["x-bypass-rls"]).toBeUndefined(); + }); + + it("sends the X-Data-Env header with the given value", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockAuthToken("test-app-token"); + t.api.mockSiteUrl({ url: t.api.baseUrl }); + + let capturedHeaders: Record = {}; + t.api.mockRoute( + "GET", + `/api/apps/${t.api.appId}/entities/Task`, + (req, res) => { + capturedHeaders = req.headers as Record; + res.json([]); + }, + ); + t.givenStdin("await base44.entities.Task.list();"); + + const result = await t.run("exec", "--data-env", "dev"); + + t.expectResult(result).toSucceed(); + expect(capturedHeaders["x-data-env"]).toBe("dev"); + }); + + it("does not send the X-Data-Env header without --data-env", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockAuthToken("test-app-token"); + t.api.mockSiteUrl({ url: t.api.baseUrl }); + + let capturedHeaders: Record = {}; + t.api.mockRoute( + "GET", + `/api/apps/${t.api.appId}/entities/Task`, + (req, res) => { + capturedHeaders = req.headers as Record; + res.json([]); + }, + ); + t.givenStdin("await base44.entities.Task.list();"); + + const result = await t.run("exec"); + + t.expectResult(result).toSucceed(); + expect(capturedHeaders["x-data-env"]).toBeUndefined(); + }); + + it("sends both headers when --privileged and --data-env are combined", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockAuthToken("test-app-token"); + t.api.mockSiteUrl({ url: t.api.baseUrl }); + + let capturedHeaders: Record = {}; + t.api.mockRoute( + "GET", + `/api/apps/${t.api.appId}/entities/Task`, + (req, res) => { + capturedHeaders = req.headers as Record; + res.json([]); + }, + ); + t.givenStdin("await base44.entities.Task.list();"); + + const result = await t.run("exec", "--privileged", "--data-env", "dev"); + + t.expectResult(result).toSucceed(); + expect(capturedHeaders["x-bypass-rls"]).toBe("true"); + expect(capturedHeaders["x-data-env"]).toBe("dev"); + }); + + it("passes BASE44_PRIVILEGED to the Deno subprocess with --privileged", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockAuthToken("test-app-token"); + t.api.mockSiteUrl({ url: "https://test-app.base44.app" }); + t.givenStdin( + 'console.log("PRIVILEGED=" + Deno.env.get("BASE44_PRIVILEGED"));', + ); + + const result = await t.run("exec", "--privileged"); + + t.expectResult(result).toSucceed(); + expect(result.stdout).toContain("PRIVILEGED=true"); + }); + + it("passes BASE44_DATA_ENV to the Deno subprocess with --data-env", async () => { + await t.givenLoggedInWithProject(fixture("basic")); + t.api.mockAuthToken("test-app-token"); + t.api.mockSiteUrl({ url: "https://test-app.base44.app" }); + t.givenStdin('console.log("DATA_ENV=" + Deno.env.get("BASE44_DATA_ENV"));'); + + const result = await t.run("exec", "--data-env", "dev"); + + t.expectResult(result).toSucceed(); + expect(result.stdout).toContain("DATA_ENV=dev"); + }); });