diff --git a/src/client.ts b/src/client.ts index b3bcc74..65a78f6 100644 --- a/src/client.ts +++ b/src/client.ts @@ -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( @@ -257,11 +258,18 @@ export function createClientFromRequest(request: Request) { userToken = authHeader.split(" ")[1]; } + // Prepare additional headers to propagate + const additionalHeaders: Record = {}; + if (stateHeader) { + additionalHeaders["Base44-State"] = stateHeader; + } + return createClient({ serverUrl: serverUrlHeader || "https://base44.app", appId, token: userToken, serviceToken: serviceRoleToken, functionsVersion: functionsVersion ?? undefined, + headers: additionalHeaders, }); } diff --git a/tests/unit/client.test.js b/tests/unit/client.test.js index f1a0c5c..5f8c265 100644 --- a/tests/unit/client.test.js +++ b/tests/unit/client.test.js @@ -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 "'); }); + + 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'); + }); }); @@ -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); + }); + }); \ No newline at end of file