Skip to content

Commit d2ea9a3

Browse files
committed
Propagate client IP header if received in request
1 parent 84de8a5 commit d2ea9a3

File tree

2 files changed

+143
-0
lines changed

2 files changed

+143
-0
lines changed

src/client.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ export function createClientFromRequest(request: Request) {
220220
const appId = request.headers.get("Base44-App-Id");
221221
const serverUrlHeader = request.headers.get("Base44-Api-Url");
222222
const functionsVersion = request.headers.get("Base44-Functions-Version");
223+
const clientIpHeader = request.headers.get("Base44-Client-IP");
223224

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

261+
// Prepare additional headers to propagate
262+
const additionalHeaders: Record<string, string> = {};
263+
if (clientIpHeader) {
264+
additionalHeaders["Base44-Client-IP"] = clientIpHeader;
265+
}
266+
260267
return createClient({
261268
serverUrl: serverUrlHeader || "https://base44.app",
262269
appId,
263270
token: userToken,
264271
serviceToken: serviceRoleToken,
265272
functionsVersion: functionsVersion ?? undefined,
273+
headers: additionalHeaders,
266274
});
267275
}

tests/unit/client.test.js

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,45 @@ describe('createClientFromRequest', () => {
219219
// Should throw error for empty headers instead of continuing silently
220220
expect(() => createClientFromRequest(mockRequest)).toThrow('Invalid authorization header format. Expected "Bearer <token>"');
221221
});
222+
223+
test('should propagate Base44-Client-IP header when present', () => {
224+
const mockRequest = {
225+
headers: {
226+
get: (name) => {
227+
const headers = {
228+
'Base44-App-Id': 'test-app-id',
229+
'Base44-Client-IP': '192.168.1.100'
230+
};
231+
return headers[name] || null;
232+
}
233+
}
234+
};
235+
236+
const client = createClientFromRequest(mockRequest);
237+
238+
expect(client).toBeDefined();
239+
const config = client.getConfig();
240+
expect(config.appId).toBe('test-app-id');
241+
});
242+
243+
test('should work without Base44-Client-IP header', () => {
244+
const mockRequest = {
245+
headers: {
246+
get: (name) => {
247+
const headers = {
248+
'Base44-App-Id': 'test-app-id'
249+
};
250+
return headers[name] || null;
251+
}
252+
}
253+
};
254+
255+
const client = createClientFromRequest(mockRequest);
256+
257+
expect(client).toBeDefined();
258+
const config = client.getConfig();
259+
expect(config.appId).toBe('test-app-id');
260+
});
222261
});
223262

224263

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

457+
test('should propagate Base44-Client-IP header in API requests when created from request', async () => {
458+
const clientIp = '192.168.1.100';
459+
460+
const mockRequest = {
461+
headers: {
462+
get: (name) => {
463+
const headers = {
464+
'Authorization': 'Bearer user-token-123',
465+
'Base44-App-Id': appId,
466+
'Base44-Api-Url': serverUrl,
467+
'Base44-Client-IP': clientIp
468+
};
469+
return headers[name] || null;
470+
}
471+
}
472+
};
473+
474+
const client = createClientFromRequest(mockRequest);
475+
476+
// Mock entities request and verify Base44-Client-IP header is present
477+
scope.get(`/api/apps/${appId}/entities/Todo`)
478+
.matchHeader('Base44-Client-IP', clientIp)
479+
.matchHeader('Authorization', 'Bearer user-token-123')
480+
.reply(200, { items: [], total: 0 });
481+
482+
// Make request
483+
await client.entities.Todo.list();
484+
485+
// Verify all mocks were called (including header match)
486+
expect(scope.isDone()).toBe(true);
487+
});
488+
489+
test('should not include Base44-Client-IP header when not present in original request', async () => {
490+
const mockRequest = {
491+
headers: {
492+
get: (name) => {
493+
const headers = {
494+
'Authorization': 'Bearer user-token-123',
495+
'Base44-App-Id': appId,
496+
'Base44-Api-Url': serverUrl
497+
};
498+
return headers[name] || null;
499+
}
500+
}
501+
};
502+
503+
const client = createClientFromRequest(mockRequest);
504+
505+
// Mock entities request and verify Base44-Client-IP header is NOT present
506+
scope.get(`/api/apps/${appId}/entities/Todo`)
507+
.matchHeader('Base44-Client-IP', (val) => !val) // Should not have this header
508+
.matchHeader('Authorization', 'Bearer user-token-123')
509+
.reply(200, { items: [], total: 0 });
510+
511+
// Make request
512+
await client.entities.Todo.list();
513+
514+
// Verify all mocks were called
515+
expect(scope.isDone()).toBe(true);
516+
});
517+
518+
test('should propagate Base44-Client-IP header in service role API requests', async () => {
519+
const clientIp = '10.0.0.50';
520+
521+
const mockRequest = {
522+
headers: {
523+
get: (name) => {
524+
const headers = {
525+
'Base44-Service-Authorization': 'Bearer service-token-123',
526+
'Base44-App-Id': appId,
527+
'Base44-Api-Url': serverUrl,
528+
'Base44-Client-IP': clientIp
529+
};
530+
return headers[name] || null;
531+
}
532+
}
533+
};
534+
535+
const client = createClientFromRequest(mockRequest);
536+
537+
// Mock service role entities request and verify Base44-Client-IP header is present
538+
scope.get(`/api/apps/${appId}/entities/User/123`)
539+
.matchHeader('Base44-Client-IP', clientIp)
540+
.matchHeader('Authorization', 'Bearer service-token-123')
541+
.reply(200, { id: '123', name: 'Test User' });
542+
543+
// Make request using service role
544+
const result = await client.asServiceRole.entities.User.get('123');
545+
546+
// Verify response
547+
expect(result.id).toBe('123');
548+
549+
// Verify all mocks were called (including header match)
550+
expect(scope.isDone()).toBe(true);
551+
});
552+
418553
});

0 commit comments

Comments
 (0)