Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/cli/deno-runtime/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {};
Expand All @@ -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");
Expand All @@ -35,10 +39,15 @@ if (!appBaseUrl) {

import { createClient } from "npm:@base44/sdk";

const customHeaders: Record<string, string> = {};
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;
Expand Down
23 changes: 21 additions & 2 deletions packages/cli/src/cli/commands/exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { readAuth } from "@/core/index.js";
interface ExecOptions {
local?: boolean;
port?: string;
privileged?: boolean;
dataEnv?: string;
}

function readStdin(): Promise<string> {
Expand Down Expand Up @@ -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;
Expand All @@ -113,6 +121,14 @@ export function getExecCommand(): Command {
"--port <number>",
`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 <environment>",
"Data environment to run against (e.g. dev, prod)",
)
.addHelpText(
"after",
`
Expand All @@ -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);
}
6 changes: 5 additions & 1 deletion packages/cli/src/core/exec/run-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ interface RunScriptOptions {
* rather than fetched via `getSiteUrl()` / `getAppUserToken()`.
*/
local?: { serverUrl: string; token: string };
privileged?: boolean;
dataEnv?: string;
}

interface RunScriptResult {
Expand All @@ -23,7 +25,7 @@ interface RunScriptResult {
export async function runScript(
options: RunScriptOptions,
): Promise<RunScriptResult> {
const { appId, code, local } = options;
const { appId, code, local, privileged, dataEnv } = options;

verifyDenoInstalled("to run scripts with exec");

Expand Down Expand Up @@ -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",
},
Expand Down
139 changes: 139 additions & 0 deletions packages/cli/tests/cli/exec.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | undefined> = {};
t.api.mockRoute(
"GET",
`/api/apps/${t.api.appId}/entities/Task`,
(req, res) => {
capturedHeaders = req.headers as Record<string, string | undefined>;
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<string, string | undefined> = {};
t.api.mockRoute(
"GET",
`/api/apps/${t.api.appId}/entities/Task`,
(req, res) => {
capturedHeaders = req.headers as Record<string, string | undefined>;
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<string, string | undefined> = {};
t.api.mockRoute(
"GET",
`/api/apps/${t.api.appId}/entities/Task`,
(req, res) => {
capturedHeaders = req.headers as Record<string, string | undefined>;
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<string, string | undefined> = {};
t.api.mockRoute(
"GET",
`/api/apps/${t.api.appId}/entities/Task`,
(req, res) => {
capturedHeaders = req.headers as Record<string, string | undefined>;
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<string, string | undefined> = {};
t.api.mockRoute(
"GET",
`/api/apps/${t.api.appId}/entities/Task`,
(req, res) => {
capturedHeaders = req.headers as Record<string, string | undefined>;
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");
});
});
Loading