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
11 changes: 11 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ export function createClientFromRequest(request: Request): Base44Client {
const serverUrlHeader = request.headers.get("Base44-Api-Url");
const functionsVersion = request.headers.get("Base44-Functions-Version");
const stateHeader = request.headers.get("Base44-State");
const dataEnvHeader = request.headers.get("X-Data-Env");

if (!appId) {
throw new Error(
Expand Down Expand Up @@ -439,6 +440,16 @@ export function createClientFromRequest(request: Request): Base44Client {
if (stateHeader) {
additionalHeaders["Base44-State"] = stateHeader;
}
// Propagate the data environment so entity operations from the function stay
// in the same environment (e.g. test data) as the triggering request. This
// matters for the user-scoped client: unlike the service token, the user JWT
// carries no data-env, so without forwarding this header the callbacks fall
// back to production data even when the app runs in test-data mode.
// Forward only the known closed set (matches the backend contract) rather
// than relaying an arbitrary attacker-supplied header value onward.
if (dataEnvHeader === "dev" || dataEnvHeader === "prod") {
additionalHeaders["X-Data-Env"] = dataEnvHeader;
}

return createClient({
serverUrl: serverUrlHeader || "https://base44.app",
Expand Down
82 changes: 82 additions & 0 deletions tests/unit/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,88 @@ describe('Service Role Authorization Headers', () => {
expect(scope.isDone()).toBe(true);
});

test('should propagate X-Data-Env header on user-scoped API requests when created from request', async () => {
const mockRequest = {
headers: {
get: (name) => {
const headers = {
'Authorization': 'Bearer user-token-123',
'Base44-App-Id': appId,
'Base44-Api-Url': serverUrl,
'X-Data-Env': 'dev'
};
return headers[name] || null;
}
}
};

const client = createClientFromRequest(mockRequest);

// The user-scoped client (not asServiceRole) must still carry the data env
// so test-mode function callbacks hit test data, not production.
scope.get(`/api/apps/${appId}/entities/Todo`)
.matchHeader('X-Data-Env', 'dev')
.matchHeader('Authorization', 'Bearer user-token-123')
.reply(200, { items: [], total: 0 });

await client.entities.Todo.list();

expect(scope.isDone()).toBe(true);
});

test('should not forward an X-Data-Env value outside the dev/prod set', async () => {
const mockRequest = {
headers: {
get: (name) => {
const headers = {
'Authorization': 'Bearer user-token-123',
'Base44-App-Id': appId,
'Base44-Api-Url': serverUrl,
'X-Data-Env': 'evil'
};
return headers[name] || null;
}
}
};

const client = createClientFromRequest(mockRequest);

scope.get(`/api/apps/${appId}/entities/Todo`)
.matchHeader('X-Data-Env', (val) => !val) // arbitrary value must not be relayed
.matchHeader('Authorization', 'Bearer user-token-123')
.reply(200, { items: [], total: 0 });

await client.entities.Todo.list();

expect(scope.isDone()).toBe(true);
});

test('should not include X-Data-Env header when not present in original request', async () => {
const mockRequest = {
headers: {
get: (name) => {
const headers = {
'Authorization': 'Bearer user-token-123',
'Base44-App-Id': appId,
'Base44-Api-Url': serverUrl
};
return headers[name] || null;
}
}
};

const client = createClientFromRequest(mockRequest);

scope.get(`/api/apps/${appId}/entities/Todo`)
.matchHeader('X-Data-Env', (val) => !val) // Should not have this header
.matchHeader('Authorization', 'Bearer user-token-123')
.reply(200, { items: [], total: 0 });

await client.entities.Todo.list();

expect(scope.isDone()).toBe(true);
});

test('should not include Base44-State header when not present in original request', async () => {
const mockRequest = {
headers: {
Expand Down
Loading