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

if (!appId) {
throw new Error(
Expand Down Expand Up @@ -257,11 +258,18 @@ export function createClientFromRequest(request: Request) {
userToken = authHeader.split(" ")[1];
}

// Prepare additional headers to propagate
const additionalHeaders: Record<string, string> = {};
if (stateHeader) {
additionalHeaders["Base44-State"] = stateHeader;
}

return createClient({
serverUrl: serverUrlHeader || "https://base44.app",
appId,
token: userToken,
serviceToken: serviceRoleToken,
functionsVersion: functionsVersion ?? undefined,
headers: additionalHeaders,
});
}
135 changes: 135 additions & 0 deletions tests/unit/client.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,45 @@ describe('createClientFromRequest', () => {
// Should throw error for empty headers instead of continuing silently
expect(() => createClientFromRequest(mockRequest)).toThrow('Invalid authorization header format. Expected "Bearer <token>"');
});

test('should propagate Base44-State header when present', () => {
const mockRequest = {
headers: {
get: (name) => {
const headers = {
'Base44-App-Id': 'test-app-id',
'Base44-State': '192.168.1.100'
};
return headers[name] || null;
}
}
};

const client = createClientFromRequest(mockRequest);

expect(client).toBeDefined();
const config = client.getConfig();
expect(config.appId).toBe('test-app-id');
});

test('should work without Base44-State header', () => {
const mockRequest = {
headers: {
get: (name) => {
const headers = {
'Base44-App-Id': 'test-app-id'
};
return headers[name] || null;
}
}
};

const client = createClientFromRequest(mockRequest);

expect(client).toBeDefined();
const config = client.getConfig();
expect(config.appId).toBe('test-app-id');
});
});


Expand Down Expand Up @@ -415,4 +454,100 @@ describe('Service Role Authorization Headers', () => {
expect(scope.isDone()).toBe(true);
});

test('should propagate Base44-State header in API requests when created from request', async () => {
const clientIp = '192.168.1.100';

const mockRequest = {
headers: {
get: (name) => {
const headers = {
'Authorization': 'Bearer user-token-123',
'Base44-App-Id': appId,
'Base44-Api-Url': serverUrl,
'Base44-State': clientIp
};
return headers[name] || null;
}
}
};

const client = createClientFromRequest(mockRequest);

// Mock entities request and verify Base44-State header is present
scope.get(`/api/apps/${appId}/entities/Todo`)
.matchHeader('Base44-State', clientIp)
.matchHeader('Authorization', 'Bearer user-token-123')
.reply(200, { items: [], total: 0 });

// Make request
await client.entities.Todo.list();

// Verify all mocks were called (including header match)
expect(scope.isDone()).toBe(true);
});

test('should not include Base44-State 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);

// Mock entities request and verify Base44-State header is NOT present
scope.get(`/api/apps/${appId}/entities/Todo`)
.matchHeader('Base44-State', (val) => !val) // Should not have this header
.matchHeader('Authorization', 'Bearer user-token-123')
.reply(200, { items: [], total: 0 });

// Make request
await client.entities.Todo.list();

// Verify all mocks were called
expect(scope.isDone()).toBe(true);
});

test('should propagate Base44-State header in service role API requests', async () => {
const clientIp = '10.0.0.50';

const mockRequest = {
headers: {
get: (name) => {
const headers = {
'Base44-Service-Authorization': 'Bearer service-token-123',
'Base44-App-Id': appId,
'Base44-Api-Url': serverUrl,
'Base44-State': clientIp
};
return headers[name] || null;
}
}
};

const client = createClientFromRequest(mockRequest);

// Mock service role entities request and verify Base44-State header is present
scope.get(`/api/apps/${appId}/entities/User/123`)
.matchHeader('Base44-State', clientIp)
.matchHeader('Authorization', 'Bearer service-token-123')
.reply(200, { id: '123', name: 'Test User' });

// Make request using service role
const result = await client.asServiceRole.entities.User.get('123');

// Verify response
expect(result.id).toBe('123');

// Verify all mocks were called (including header match)
expect(scope.isDone()).toBe(true);
});

});