From 6ef2783717bd5cab0feb852e0335f1657e7da808 Mon Sep 17 00:00:00 2001 From: Netanel Gilad Date: Thu, 14 Aug 2025 10:21:22 +0300 Subject: [PATCH 1/9] service role support --- src/client.ts | 95 +++++++++++++++++++++++++++++++++------------ src/index.ts | 4 +- src/modules/auth.ts | 7 +++- 3 files changed, 78 insertions(+), 28 deletions(-) diff --git a/src/client.ts b/src/client.ts index d60687f..8a040e0 100644 --- a/src/client.ts +++ b/src/client.ts @@ -10,44 +10,41 @@ import { createFunctionsModule } from "./modules/functions.js"; * @param {Object} config - Client configuration * @param {string} [config.serverUrl='https://base44.app'] - API server URL * @param {string|number} config.appId - Application ID - * @param {string} [config.env='prod'] - Environment ('prod' or 'dev') * @param {string} [config.token] - Authentication token + * @param {string} [config.serviceToken] - Service role authentication token * @param {boolean} [config.requiresAuth=false] - Whether the app requires authentication * @returns {Object} Base44 client instance */ export function createClient(config: { serverUrl?: string; appId: string; - env?: string; token?: string; + serviceToken?: string; requiresAuth?: boolean; }) { const { serverUrl = "https://base44.app", appId, - env = "prod", token, + serviceToken, requiresAuth = false, } = config; - // Create the base axios client const axiosClient = createAxiosClient({ baseURL: `${serverUrl}/api`, headers: { "X-App-Id": String(appId), - "X-Environment": env, }, token, - requiresAuth, // Pass requiresAuth to axios client - appId, // Pass appId for login redirect - serverUrl, // Pass serverUrl for login redirect + requiresAuth, + appId, + serverUrl, }); const functionsAxiosClient = createAxiosClient({ baseURL: `${serverUrl}/api`, headers: { "X-App-Id": String(appId), - "X-Environment": env, }, token, requiresAuth, @@ -56,18 +53,46 @@ export function createClient(config: { interceptResponses: false, }); - // Create modules - const entities = createEntitiesModule(axiosClient, appId); - const integrations = createIntegrationsModule(axiosClient, appId); - const auth = createAuthModule(axiosClient, appId, serverUrl); - const functions = createFunctionsModule(functionsAxiosClient, appId); + const serviceRoleAxiosClient = createAxiosClient({ + baseURL: `${serverUrl}/api`, + headers: { + "X-App-Id": String(appId), + }, + token: serviceToken, + serverUrl, + appId, + }); + + const serviceRoleFunctionsAxiosClient = createAxiosClient({ + baseURL: `${serverUrl}/api`, + headers: { + "X-App-Id": String(appId), + }, + token: serviceToken, + serverUrl, + appId, + interceptResponses: false, + }); + + const userModules = { + entities: createEntitiesModule(axiosClient, appId), + integrations: createIntegrationsModule(axiosClient, appId), + auth: createAuthModule(axiosClient, functionsAxiosClient, appId), + functions: createFunctionsModule(functionsAxiosClient, appId), + }; + + const serviceRoleModules = { + entities: createEntitiesModule(serviceRoleAxiosClient, appId), + integrations: createIntegrationsModule(serviceRoleAxiosClient, appId), + functions: createFunctionsModule(serviceRoleFunctionsAxiosClient, appId), + }; // Always try to get token from localStorage or URL parameters if (typeof window !== "undefined") { // Get token from URL or localStorage const accessToken = token || getAccessToken(); if (accessToken) { - auth.setToken(accessToken); + userModules.auth.setToken(accessToken); } } @@ -76,30 +101,28 @@ export function createClient(config: { // We perform this check asynchronously to not block client creation setTimeout(async () => { try { - const isAuthenticated = await auth.isAuthenticated(); + const isAuthenticated = await userModules.auth.isAuthenticated(); if (!isAuthenticated) { - auth.redirectToLogin(window.location.href); + userModules.auth.redirectToLogin(window.location.href); } } catch (error) { console.error("Authentication check failed:", error); - auth.redirectToLogin(window.location.href); + userModules.auth.redirectToLogin(window.location.href); } }, 0); } // Assemble and return the client return { - entities, - integrations, - auth, - functions, + ...userModules, + asService: serviceRoleModules, /** * Set authentication token for all requests * @param {string} newToken - New auth token */ setToken(newToken: string) { - auth.setToken(newToken); + userModules.auth.setToken(newToken); }, /** @@ -110,9 +133,33 @@ export function createClient(config: { return { serverUrl, appId, - env, requiresAuth, }; }, }; } + +export function createClientFromRequest(request: Request) { + const authHeader = request.headers.get("Authorization"); + const serviceRoleAuthHeader = request.headers.get( + "Base44-Service-Authorization" + ); + const appId = request.headers.get("Base44-App-Id"); + const serverUrlHeader = request.headers.get("Base44-Api-Url"); + + if (!appId) { + throw new Error( + "Base44-App-Id header is required, but is was not found on the request" + ); + } + + const serviceRoleToken = serviceRoleAuthHeader?.split(" ")[1]; + const userToken = authHeader?.split(" ")[1]; + + return createClient({ + serverUrl: serverUrlHeader || "https://base44.app", + appId, + token: userToken, + serviceToken: serviceRoleToken, + }); +} diff --git a/src/index.ts b/src/index.ts index b3461a3..2002286 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import { createClient } from "./client.js"; +import { createClient, createClientFromRequest } from "./client.js"; import { Base44Error } from "./utils/axios-client.js"; import { getAccessToken, @@ -9,8 +9,8 @@ import { export { createClient, + createClientFromRequest, Base44Error, - // Export auth utilities for easier access getAccessToken, saveAccessToken, removeAccessToken, diff --git a/src/modules/auth.ts b/src/modules/auth.ts index c18bf2e..0eae2b5 100644 --- a/src/modules/auth.ts +++ b/src/modules/auth.ts @@ -9,8 +9,8 @@ import { AxiosInstance } from "axios"; */ export function createAuthModule( axios: AxiosInstance, - appId: string, - serverUrl: string + functionsAxiosClient: AxiosInstance, + appId: string ) { return { /** @@ -98,6 +98,9 @@ export function createAuthModule( if (!token) return; axios.defaults.headers.common["Authorization"] = `Bearer ${token}`; + functionsAxiosClient.defaults.headers.common[ + "Authorization" + ] = `Bearer ${token}`; // Save token to localStorage if requested if ( From da0e2754e74eab2cbe8385e56f0de8786ef85478 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 07:27:10 +0000 Subject: [PATCH 2/9] fix: remove env property from client tests - Remove env parameter from createClient calls in tests - Remove env property expectations from getConfig() assertions - Tests now align with current client implementation Co-authored-by: Netanel Gilad --- tests/unit/client.test.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/unit/client.test.js b/tests/unit/client.test.js index 73b0572..cf9b565 100644 --- a/tests/unit/client.test.js +++ b/tests/unit/client.test.js @@ -15,7 +15,6 @@ describe('Client Creation', () => { const config = client.getConfig(); expect(config.appId).toBe('test-app-id'); expect(config.serverUrl).toBe('https://base44.app'); - expect(config.env).toBe('prod'); expect(config.requiresAuth).toBe(false); }); @@ -23,7 +22,6 @@ describe('Client Creation', () => { const client = createClient({ appId: 'test-app-id', serverUrl: 'https://custom-server.com', - env: 'dev', requiresAuth: true, token: 'test-token', }); @@ -33,7 +31,6 @@ describe('Client Creation', () => { const config = client.getConfig(); expect(config.appId).toBe('test-app-id'); expect(config.serverUrl).toBe('https://custom-server.com'); - expect(config.env).toBe('dev'); expect(config.requiresAuth).toBe(true); }); }); \ No newline at end of file From bc9a374c7c2dcd6827f7cd2b3913e5ba72e3af11 Mon Sep 17 00:00:00 2001 From: Netanel Gilad Date: Thu, 14 Aug 2025 10:42:10 +0300 Subject: [PATCH 3/9] asService --- src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index 8a040e0..8a337ff 100644 --- a/src/client.ts +++ b/src/client.ts @@ -115,7 +115,7 @@ export function createClient(config: { // Assemble and return the client return { ...userModules, - asService: serviceRoleModules, + asServiceRole: serviceRoleModules, /** * Set authentication token for all requests From c3a1d6f514662771e76346ce7de6de83224af293 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 07:49:43 +0000 Subject: [PATCH 4/9] test: add comprehensive unit tests for service role feature - Add tests for serviceToken parameter in createClient - Add tests for createClientFromRequest function with various header scenarios - Add tests for asServiceRole API functionality and module separation - Test edge cases including malformed headers and missing parameters - All 13 client tests now pass including service role functionality Co-authored-by: Netanel Gilad --- tests/unit/client.test.js | 232 +++++++++++++++++++++++++++++++++++++- 1 file changed, 231 insertions(+), 1 deletion(-) diff --git a/tests/unit/client.test.js b/tests/unit/client.test.js index cf9b565..72b44b1 100644 --- a/tests/unit/client.test.js +++ b/tests/unit/client.test.js @@ -1,4 +1,4 @@ -import { createClient } from '../../src/index.ts'; +import { createClient, createClientFromRequest } from '../../src/index.ts'; import { describe, test, expect } from 'vitest'; describe('Client Creation', () => { @@ -33,4 +33,234 @@ describe('Client Creation', () => { expect(config.serverUrl).toBe('https://custom-server.com'); expect(config.requiresAuth).toBe(true); }); + + test('should create a client with service token', () => { + const client = createClient({ + appId: 'test-app-id', + serviceToken: 'service-token-123', + }); + + expect(client).toBeDefined(); + expect(client.entities).toBeDefined(); + expect(client.integrations).toBeDefined(); + expect(client.auth).toBeDefined(); + expect(client.asServiceRole).toBeDefined(); + expect(client.asServiceRole.entities).toBeDefined(); + expect(client.asServiceRole.integrations).toBeDefined(); + expect(client.asServiceRole.functions).toBeDefined(); + // Service role should not have auth module + expect(client.asServiceRole.auth).toBeUndefined(); + }); + + test('should create a client with both user token and service token', () => { + const client = createClient({ + appId: 'test-app-id', + token: 'user-token-123', + serviceToken: 'service-token-123', + requiresAuth: true, + }); + + expect(client).toBeDefined(); + expect(client.entities).toBeDefined(); + expect(client.integrations).toBeDefined(); + expect(client.auth).toBeDefined(); + expect(client.asServiceRole).toBeDefined(); + expect(client.asServiceRole.entities).toBeDefined(); + expect(client.asServiceRole.integrations).toBeDefined(); + expect(client.asServiceRole.functions).toBeDefined(); + expect(client.asServiceRole.auth).toBeUndefined(); + }); +}); + +describe('createClientFromRequest', () => { + test('should create client from request with all headers', () => { + const mockRequest = { + headers: { + get: (name) => { + const headers = { + 'Authorization': 'Bearer user-token-123', + 'Base44-Service-Authorization': 'Bearer service-token-123', + 'Base44-App-Id': 'test-app-id', + 'Base44-Api-Url': 'https://custom-server.com' + }; + return headers[name] || null; + } + } + }; + + const client = createClientFromRequest(mockRequest); + + expect(client).toBeDefined(); + expect(client.entities).toBeDefined(); + expect(client.integrations).toBeDefined(); + expect(client.auth).toBeDefined(); + expect(client.asServiceRole).toBeDefined(); + + const config = client.getConfig(); + expect(config.appId).toBe('test-app-id'); + expect(config.serverUrl).toBe('https://custom-server.com'); + }); + + test('should create client from request with minimal headers', () => { + const mockRequest = { + headers: { + get: (name) => { + const headers = { + 'Base44-App-Id': 'minimal-app-id' + }; + return headers[name] || null; + } + } + }; + + const client = createClientFromRequest(mockRequest); + + expect(client).toBeDefined(); + const config = client.getConfig(); + expect(config.appId).toBe('minimal-app-id'); + expect(config.serverUrl).toBe('https://base44.app'); // Default value + }); + + test('should create client with only user token', () => { + const mockRequest = { + headers: { + get: (name) => { + const headers = { + 'Authorization': 'Bearer user-only-token', + 'Base44-App-Id': 'user-app-id' + }; + return headers[name] || null; + } + } + }; + + const client = createClientFromRequest(mockRequest); + + expect(client).toBeDefined(); + expect(client.auth).toBeDefined(); + expect(client.asServiceRole).toBeDefined(); + }); + + test('should create client with only service token', () => { + const mockRequest = { + headers: { + get: (name) => { + const headers = { + 'Base44-Service-Authorization': 'Bearer service-only-token', + 'Base44-App-Id': 'service-app-id' + }; + return headers[name] || null; + } + } + }; + + const client = createClientFromRequest(mockRequest); + + expect(client).toBeDefined(); + expect(client.auth).toBeDefined(); + expect(client.asServiceRole).toBeDefined(); + }); + + test('should throw error when Base44-App-Id header is missing', () => { + const mockRequest = { + headers: { + get: (name) => { + const headers = { + 'Authorization': 'Bearer some-token' + }; + return headers[name] || null; + } + } + }; + + expect(() => createClientFromRequest(mockRequest)).toThrow( + 'Base44-App-Id header is required, but is was not found on the request' + ); + }); + + test('should handle malformed authorization headers gracefully', () => { + const mockRequest = { + headers: { + get: (name) => { + const headers = { + 'Authorization': 'InvalidFormat', + 'Base44-Service-Authorization': 'AlsoInvalid', + 'Base44-App-Id': 'test-app-id' + }; + return headers[name] || null; + } + } + }; + + const client = createClientFromRequest(mockRequest); + + expect(client).toBeDefined(); + // Client should still be created even with malformed headers + expect(client.entities).toBeDefined(); + expect(client.asServiceRole).toBeDefined(); + }); + + test('should handle empty authorization headers', () => { + const mockRequest = { + headers: { + get: (name) => { + const headers = { + 'Authorization': '', + 'Base44-Service-Authorization': '', + 'Base44-App-Id': 'test-app-id' + }; + return headers[name] || null; + } + } + }; + + const client = createClientFromRequest(mockRequest); + + expect(client).toBeDefined(); + expect(client.entities).toBeDefined(); + expect(client.asServiceRole).toBeDefined(); + }); +}); + +describe('Service Role API', () => { + test('should have separate service role modules', () => { + const client = createClient({ + appId: 'test-app-id', + serviceToken: 'service-token-123', + }); + + // User modules should exist + expect(client.entities).toBeDefined(); + expect(client.integrations).toBeDefined(); + expect(client.auth).toBeDefined(); + expect(client.functions).toBeDefined(); + + // Service role modules should exist + expect(client.asServiceRole).toBeDefined(); + expect(client.asServiceRole.entities).toBeDefined(); + expect(client.asServiceRole.integrations).toBeDefined(); + expect(client.asServiceRole.functions).toBeDefined(); + + // Service role should NOT have auth module + expect(client.asServiceRole.auth).toBeUndefined(); + + // They should be different instances + expect(client.entities).not.toBe(client.asServiceRole.entities); + expect(client.integrations).not.toBe(client.asServiceRole.integrations); + expect(client.functions).not.toBe(client.asServiceRole.functions); + }); + + test('should work without service token', () => { + const client = createClient({ + appId: 'test-app-id', + token: 'user-token-123', + }); + + // Service role should still exist but without token + expect(client.asServiceRole).toBeDefined(); + expect(client.asServiceRole.entities).toBeDefined(); + expect(client.asServiceRole.integrations).toBeDefined(); + expect(client.asServiceRole.functions).toBeDefined(); + expect(client.asServiceRole.auth).toBeUndefined(); + }); }); \ No newline at end of file From fc4f2f1e32ba30f2658f4df99577bead53f31016 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 08:04:26 +0000 Subject: [PATCH 5/9] test: add service role authorization header tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive unit tests to verify that service role operations send correct authorization headers: - Tests user vs service role token separation - Tests service role entities, integrations, and functions operations - Tests correct Authorization header is sent for each operation type - Tests operations work without tokens when no token is provided - Follows existing test patterns using nock for HTTP mocking 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Netanel Gilad --- tests/unit/client.test.js | 222 +++++++++++++++++++++++++++++++++++++- 1 file changed, 221 insertions(+), 1 deletion(-) diff --git a/tests/unit/client.test.js b/tests/unit/client.test.js index 72b44b1..89f816b 100644 --- a/tests/unit/client.test.js +++ b/tests/unit/client.test.js @@ -1,5 +1,6 @@ import { createClient, createClientFromRequest } from '../../src/index.ts'; -import { describe, test, expect } from 'vitest'; +import { describe, test, expect, beforeEach, afterEach } from 'vitest'; +import nock from 'nock'; describe('Client Creation', () => { test('should create a client with default options', () => { @@ -263,4 +264,223 @@ describe('Service Role API', () => { expect(client.asServiceRole.functions).toBeDefined(); expect(client.asServiceRole.auth).toBeUndefined(); }); +}); + +describe('Service Role Authorization Headers', () => { + + let scope; + const appId = 'test-app-id'; + const serverUrl = 'https://api.base44.com'; + + beforeEach(() => { + // Create a nock scope for mocking API calls + scope = nock(serverUrl); + + // Enable request debugging for Nock + nock.disableNetConnect(); + nock.emitter.on('no match', (req) => { + console.log(`Nock: No match for ${req.method} ${req.path}`); + console.log('Headers:', req.getHeaders()); + }); + }); + + afterEach(() => { + // Clean up any pending mocks + nock.cleanAll(); + nock.emitter.removeAllListeners('no match'); + nock.enableNetConnect(); + }); + + test('should use user token for regular client operations and service token for service role operations', async () => { + const userToken = 'user-token-123'; + const serviceToken = 'service-token-456'; + + const client = createClient({ + serverUrl, + appId, + token: userToken, + serviceToken: serviceToken, + }); + + // Mock user entities request (should use user token) + scope.get(`/api/apps/${appId}/entities/Todo`) + .matchHeader('Authorization', `Bearer ${userToken}`) + .reply(200, { items: [], total: 0 }); + + // Mock service role entities request (should use service token) + scope.get(`/api/apps/${appId}/entities/Todo`) + .matchHeader('Authorization', `Bearer ${serviceToken}`) + .reply(200, { items: [], total: 0 }); + + // Make requests + await client.entities.Todo.list(); + await client.asServiceRole.entities.Todo.list(); + + // Verify all mocks were called + expect(scope.isDone()).toBe(true); + }); + + test('should use service token for service role entities operations', async () => { + const serviceToken = 'service-token-only-123'; + + const client = createClient({ + serverUrl, + appId, + serviceToken: serviceToken, + }); + + // Mock service role entities request + scope.get(`/api/apps/${appId}/entities/User/123`) + .matchHeader('Authorization', `Bearer ${serviceToken}`) + .reply(200, { id: '123', name: 'Test User' }); + + // Make request + const result = await client.asServiceRole.entities.User.get('123'); + + // Verify response + expect(result.id).toBe('123'); + expect(result.name).toBe('Test User'); + + // Verify all mocks were called + expect(scope.isDone()).toBe(true); + }); + + test('should use service token for service role integrations operations', async () => { + const serviceToken = 'service-token-integration-456'; + + const client = createClient({ + serverUrl, + appId, + serviceToken: serviceToken, + }); + + // Mock service role integrations request + scope.post(`/api/apps/${appId}/integration-endpoints/Core/SendEmail`) + .matchHeader('Authorization', `Bearer ${serviceToken}`) + .reply(200, { success: true, messageId: '123' }); + + // Make request + const result = await client.asServiceRole.integrations.Core.SendEmail({ + to: 'test@example.com', + subject: 'Test', + body: 'Test message' + }); + + // Verify response + expect(result.success).toBe(true); + expect(result.messageId).toBe('123'); + + // Verify all mocks were called + expect(scope.isDone()).toBe(true); + }); + + test('should use service token for service role functions operations', async () => { + const serviceToken = 'service-token-functions-789'; + + const client = createClient({ + serverUrl, + appId, + serviceToken: serviceToken, + }); + + // Mock service role functions request + scope.post(`/api/apps/${appId}/functions/testFunction`, { param: 'test' }) + .matchHeader('Authorization', `Bearer ${serviceToken}`) + .reply(200, { result: 'function executed' }); + + // Make request + const result = await client.asServiceRole.functions.invoke('testFunction', { + param: 'test' + }); + + // Verify response + expect(result.data.result).toBe('function executed'); + + // Verify all mocks were called + expect(scope.isDone()).toBe(true); + }); + + test('should use user token for regular operations when both tokens are present', async () => { + const userToken = 'user-token-regular-123'; + const serviceToken = 'service-token-regular-456'; + + const client = createClient({ + serverUrl, + appId, + token: userToken, + serviceToken: serviceToken, + }); + + // Mock regular user entities request (should use user token) + scope.get(`/api/apps/${appId}/entities/Task`) + .matchHeader('Authorization', `Bearer ${userToken}`) + .reply(200, { items: [{ id: 'task1', title: 'User Task' }], total: 1 }); + + // Mock regular integrations request (should use user token) + scope.post(`/api/apps/${appId}/integration-endpoints/Core/SendEmail`) + .matchHeader('Authorization', `Bearer ${userToken}`) + .reply(200, { success: true, messageId: 'email123' }); + + // Make requests using regular client (not service role) + const taskResult = await client.entities.Task.list(); + const emailResult = await client.integrations.Core.SendEmail({ + to: 'user@example.com', + subject: 'User Test', + body: 'User message' + }); + + // Verify responses + expect(taskResult.items[0].title).toBe('User Task'); + expect(emailResult.success).toBe(true); + expect(emailResult.messageId).toBe('email123'); + + // Verify all mocks were called + expect(scope.isDone()).toBe(true); + }); + + test('should work without authorization header when no tokens are provided', async () => { + const client = createClient({ + serverUrl, + appId, + }); + + // Mock request without authorization header + scope.get(`/api/apps/${appId}/entities/PublicData`) + .matchHeader('Authorization', (val) => !val) // Should not have Authorization header + .reply(200, { items: [{ id: 'public1', data: 'public' }], total: 1 }); + + // Make request + const result = await client.entities.PublicData.list(); + + // Verify response + expect(result.items[0].data).toBe('public'); + + // Verify all mocks were called + expect(scope.isDone()).toBe(true); + }); + + test('should work without authorization header for service role when no service token is provided', async () => { + const userToken = 'user-only-token-123'; + + const client = createClient({ + serverUrl, + appId, + token: userToken, + // No serviceToken provided + }); + + // Mock service role request without authorization header (since no service token) + scope.get(`/api/apps/${appId}/entities/PublicData`) + .matchHeader('Authorization', (val) => !val) // Should not have Authorization header + .reply(200, { items: [{ id: 'public1', data: 'service role public' }], total: 1 }); + + // Make request using service role (should work without service token) + const result = await client.asServiceRole.entities.PublicData.list(); + + // Verify response + expect(result.items[0].data).toBe('service role public'); + + // Verify all mocks were called + expect(scope.isDone()).toBe(true); + }); }); \ No newline at end of file From 62f34187b057fe50b98abe96c7817f6e9cc3256b Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 08:11:57 +0000 Subject: [PATCH 6/9] feat: convert asServiceRole to getter with error handling - Convert asServiceRole from property to getter that validates service token presence - Throws descriptive error when accessed without service token - Update all existing tests to expect error when no service token provided - Add new tests for error scenarios and multiple access attempts - Maintains backward compatibility for clients with service tokens Co-authored-by: Netanel Gilad --- src/client.ts | 16 ++++++++++-- tests/unit/client.test.js | 51 +++++++++++++++++++++------------------ 2 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/client.ts b/src/client.ts index 8a337ff..003e064 100644 --- a/src/client.ts +++ b/src/client.ts @@ -113,9 +113,8 @@ export function createClient(config: { } // Assemble and return the client - return { + const client = { ...userModules, - asServiceRole: serviceRoleModules, /** * Set authentication token for all requests @@ -136,7 +135,20 @@ export function createClient(config: { requiresAuth, }; }, + + /** + * Access service role modules - throws error if no service token was provided + * @throws {Error} When accessed without a service token + */ + get asServiceRole() { + if (!serviceToken) { + throw new Error('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); + } + return serviceRoleModules; + } }; + + return client; } export function createClientFromRequest(request: Request) { diff --git a/tests/unit/client.test.js b/tests/unit/client.test.js index 89f816b..a321e43 100644 --- a/tests/unit/client.test.js +++ b/tests/unit/client.test.js @@ -17,6 +17,9 @@ describe('Client Creation', () => { expect(config.appId).toBe('test-app-id'); expect(config.serverUrl).toBe('https://base44.app'); expect(config.requiresAuth).toBe(false); + + // Should throw error when accessing asServiceRole without service token + expect(() => client.asServiceRole).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); }); test('should create a client with custom options', () => { @@ -71,6 +74,18 @@ describe('Client Creation', () => { expect(client.asServiceRole.functions).toBeDefined(); expect(client.asServiceRole.auth).toBeUndefined(); }); + + test('should throw error when accessing asServiceRole multiple times without service token', () => { + const client = createClient({ + appId: 'test-app-id', + token: 'user-token-123', + }); + + // Should throw error every time asServiceRole is accessed + expect(() => client.asServiceRole).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); + expect(() => client.asServiceRole).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); + expect(() => client.asServiceRole.entities).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); + }); }); describe('createClientFromRequest', () => { @@ -139,7 +154,8 @@ describe('createClientFromRequest', () => { expect(client).toBeDefined(); expect(client.auth).toBeDefined(); - expect(client.asServiceRole).toBeDefined(); + // asServiceRole should throw error when accessed without service token + expect(() => client.asServiceRole).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); }); test('should create client with only service token', () => { @@ -198,7 +214,8 @@ describe('createClientFromRequest', () => { expect(client).toBeDefined(); // Client should still be created even with malformed headers expect(client.entities).toBeDefined(); - expect(client.asServiceRole).toBeDefined(); + // asServiceRole should throw error when accessed without valid service token (malformed doesn't count) + expect(() => client.asServiceRole).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); }); test('should handle empty authorization headers', () => { @@ -219,7 +236,8 @@ describe('createClientFromRequest', () => { expect(client).toBeDefined(); expect(client.entities).toBeDefined(); - expect(client.asServiceRole).toBeDefined(); + // asServiceRole should throw error when accessed without service token (empty doesn't count) + expect(() => client.asServiceRole).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); }); }); @@ -251,18 +269,14 @@ describe('Service Role API', () => { expect(client.functions).not.toBe(client.asServiceRole.functions); }); - test('should work without service token', () => { + test('should throw error when accessing asServiceRole without service token', () => { const client = createClient({ appId: 'test-app-id', token: 'user-token-123', }); - // Service role should still exist but without token - expect(client.asServiceRole).toBeDefined(); - expect(client.asServiceRole.entities).toBeDefined(); - expect(client.asServiceRole.integrations).toBeDefined(); - expect(client.asServiceRole.functions).toBeDefined(); - expect(client.asServiceRole.auth).toBeUndefined(); + // Service role should throw error when accessed without token + expect(() => client.asServiceRole).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); }); }); @@ -459,7 +473,7 @@ describe('Service Role Authorization Headers', () => { expect(scope.isDone()).toBe(true); }); - test('should work without authorization header for service role when no service token is provided', async () => { + test('should throw error when accessing asServiceRole without service token', async () => { const userToken = 'user-only-token-123'; const client = createClient({ @@ -469,18 +483,7 @@ describe('Service Role Authorization Headers', () => { // No serviceToken provided }); - // Mock service role request without authorization header (since no service token) - scope.get(`/api/apps/${appId}/entities/PublicData`) - .matchHeader('Authorization', (val) => !val) // Should not have Authorization header - .reply(200, { items: [{ id: 'public1', data: 'service role public' }], total: 1 }); - - // Make request using service role (should work without service token) - const result = await client.asServiceRole.entities.PublicData.list(); - - // Verify response - expect(result.items[0].data).toBe('service role public'); - - // Verify all mocks were called - expect(scope.isDone()).toBe(true); + // Should throw error when accessing asServiceRole without service token + expect(() => client.asServiceRole).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); }); }); \ No newline at end of file From 9805c2a9f92297aead1907f7f9e7fba91ba54885 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 08:23:43 +0000 Subject: [PATCH 7/9] refactor: clean up redundant service role tests and improve error handling - Remove redundant test for multiple asServiceRole access attempts - Remove repetitive authorization header validation tests - Update malformed/empty header tests to throw informative errors - Add proper header validation in createClientFromRequest function - Remove duplicate Service Role API tests already covered elsewhere Co-authored-by: Netanel Gilad --- src/client.ts | 19 ++++++++- tests/unit/client.test.js | 87 ++++----------------------------------- 2 files changed, 25 insertions(+), 81 deletions(-) diff --git a/src/client.ts b/src/client.ts index 003e064..f8061db 100644 --- a/src/client.ts +++ b/src/client.ts @@ -165,8 +165,23 @@ export function createClientFromRequest(request: Request) { ); } - const serviceRoleToken = serviceRoleAuthHeader?.split(" ")[1]; - const userToken = authHeader?.split(" ")[1]; + // Validate authorization header formats + let serviceRoleToken: string | undefined; + let userToken: string | undefined; + + if (serviceRoleAuthHeader !== null) { + if (serviceRoleAuthHeader === '' || !serviceRoleAuthHeader.startsWith('Bearer ') || serviceRoleAuthHeader.split(' ').length !== 2) { + throw new Error('Invalid authorization header format. Expected "Bearer "'); + } + serviceRoleToken = serviceRoleAuthHeader.split(' ')[1]; + } + + if (authHeader !== null) { + if (authHeader === '' || !authHeader.startsWith('Bearer ') || authHeader.split(' ').length !== 2) { + throw new Error('Invalid authorization header format. Expected "Bearer "'); + } + userToken = authHeader.split(' ')[1]; + } return createClient({ serverUrl: serverUrlHeader || "https://base44.app", diff --git a/tests/unit/client.test.js b/tests/unit/client.test.js index a321e43..f1a0c5c 100644 --- a/tests/unit/client.test.js +++ b/tests/unit/client.test.js @@ -75,17 +75,6 @@ describe('Client Creation', () => { expect(client.asServiceRole.auth).toBeUndefined(); }); - test('should throw error when accessing asServiceRole multiple times without service token', () => { - const client = createClient({ - appId: 'test-app-id', - token: 'user-token-123', - }); - - // Should throw error every time asServiceRole is accessed - expect(() => client.asServiceRole).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); - expect(() => client.asServiceRole).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); - expect(() => client.asServiceRole.entities).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); - }); }); describe('createClientFromRequest', () => { @@ -154,7 +143,7 @@ describe('createClientFromRequest', () => { expect(client).toBeDefined(); expect(client.auth).toBeDefined(); - // asServiceRole should throw error when accessed without service token + // Should throw error when accessing asServiceRole without service token expect(() => client.asServiceRole).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); }); @@ -195,7 +184,7 @@ describe('createClientFromRequest', () => { ); }); - test('should handle malformed authorization headers gracefully', () => { + test('should throw error for malformed authorization headers', () => { const mockRequest = { headers: { get: (name) => { @@ -209,16 +198,11 @@ describe('createClientFromRequest', () => { } }; - const client = createClientFromRequest(mockRequest); - - expect(client).toBeDefined(); - // Client should still be created even with malformed headers - expect(client.entities).toBeDefined(); - // asServiceRole should throw error when accessed without valid service token (malformed doesn't count) - expect(() => client.asServiceRole).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); + // Should throw error for malformed headers instead of continuing silently + expect(() => createClientFromRequest(mockRequest)).toThrow('Invalid authorization header format. Expected "Bearer "'); }); - test('should handle empty authorization headers', () => { + test('should throw error for empty authorization headers', () => { const mockRequest = { headers: { get: (name) => { @@ -227,58 +211,16 @@ describe('createClientFromRequest', () => { 'Base44-Service-Authorization': '', 'Base44-App-Id': 'test-app-id' }; - return headers[name] || null; + return headers[name] === '' ? '' : headers[name] || null; } } }; - const client = createClientFromRequest(mockRequest); - - expect(client).toBeDefined(); - expect(client.entities).toBeDefined(); - // asServiceRole should throw error when accessed without service token (empty doesn't count) - expect(() => client.asServiceRole).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); + // Should throw error for empty headers instead of continuing silently + expect(() => createClientFromRequest(mockRequest)).toThrow('Invalid authorization header format. Expected "Bearer "'); }); }); -describe('Service Role API', () => { - test('should have separate service role modules', () => { - const client = createClient({ - appId: 'test-app-id', - serviceToken: 'service-token-123', - }); - - // User modules should exist - expect(client.entities).toBeDefined(); - expect(client.integrations).toBeDefined(); - expect(client.auth).toBeDefined(); - expect(client.functions).toBeDefined(); - - // Service role modules should exist - expect(client.asServiceRole).toBeDefined(); - expect(client.asServiceRole.entities).toBeDefined(); - expect(client.asServiceRole.integrations).toBeDefined(); - expect(client.asServiceRole.functions).toBeDefined(); - - // Service role should NOT have auth module - expect(client.asServiceRole.auth).toBeUndefined(); - - // They should be different instances - expect(client.entities).not.toBe(client.asServiceRole.entities); - expect(client.integrations).not.toBe(client.asServiceRole.integrations); - expect(client.functions).not.toBe(client.asServiceRole.functions); - }); - - test('should throw error when accessing asServiceRole without service token', () => { - const client = createClient({ - appId: 'test-app-id', - token: 'user-token-123', - }); - - // Service role should throw error when accessed without token - expect(() => client.asServiceRole).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); - }); -}); describe('Service Role Authorization Headers', () => { @@ -473,17 +415,4 @@ describe('Service Role Authorization Headers', () => { expect(scope.isDone()).toBe(true); }); - test('should throw error when accessing asServiceRole without service token', async () => { - const userToken = 'user-only-token-123'; - - const client = createClient({ - serverUrl, - appId, - token: userToken, - // No serviceToken provided - }); - - // Should throw error when accessing asServiceRole without service token - expect(() => client.asServiceRole).toThrow('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.'); - }); }); \ No newline at end of file From 67658844d9ec2bf79978297bc837320bb10c0191 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 08:58:28 +0000 Subject: [PATCH 8/9] test: add authorization header test for function invocations This test verifies the bug fix that ensures functions are invoked with the user token when provided, testing that the Authorization header is correctly sent with function calls. Co-authored-by: Netanel Gilad --- tests/unit/functions.test.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/unit/functions.test.ts b/tests/unit/functions.test.ts index 11101c4..3cd7d59 100644 --- a/tests/unit/functions.test.ts +++ b/tests/unit/functions.test.ts @@ -408,4 +408,39 @@ describe("Functions Module", () => { // Verify all mocks were called expect(scope.isDone()).toBe(true); }); + + test("should send user token as Authorization header when invoking functions", async () => { + const functionName = "testAuth"; + const userToken = "user-test-token"; + const functionData = { + test: "data", + }; + + // Create client with user token + const authenticatedBase44 = createClient({ + serverUrl, + appId, + token: userToken, + }); + + // Mock the API response, verifying the Authorization header + scope + .post(`/api/apps/${appId}/functions/${functionName}`, functionData) + .matchHeader("Content-Type", "application/json") + .matchHeader("Authorization", `Bearer ${userToken}`) + .reply(200, { + success: true, + authenticated: true, + }); + + // Call the function + const result = await authenticatedBase44.functions.invoke(functionName, functionData); + + // Verify the response + expect(result.data.success).toBe(true); + expect(result.data.authenticated).toBe(true); + + // Verify all mocks were called + expect(scope.isDone()).toBe(true); + }); }); From 135ce62ee8457f7a3f406593f86affae9c29b002 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 10:27:52 +0000 Subject: [PATCH 9/9] docs: update README with service role authentication features - Add service role authentication section with examples - Add server-side usage section with createClientFromRequest - Update basic setup to include serviceToken parameter - Fix outdated accessToken parameter to token - Add Functions section with invocation examples - Add TypeScript examples for service role operations - Remove deprecated env parameter from examples Co-authored-by: Netanel Gilad --- README.md | 120 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 114 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2e38dee..e0d919e 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,10 @@ import { createClient } from '@base44/sdk'; // Create a client instance const base44 = createClient({ serverUrl: 'https://base44.app', // Optional, defaults to 'https://base44.app' - appId: 'your-app-id', // Required - env: 'prod', // Optional, defaults to 'prod' - token: 'your-token', // Optional - autoInitAuth: true, // Optional, defaults to true - auto-detects tokens from URL or localStorage + appId: 'your-app-id', // Required + token: 'your-user-token', // Optional, for user authentication + serviceToken: 'your-service-token', // Optional, for service role authentication + autoInitAuth: true, // Optional, defaults to true - auto-detects tokens from URL or localStorage }); ``` @@ -63,6 +63,77 @@ const newProducts = await base44.entities.Product.bulkCreate([ ]); ``` +### Service Role Authentication + +Service role authentication allows server-side applications to perform operations with elevated privileges. This is useful for administrative tasks, background jobs, and server-to-server communication. + +```javascript +import { createClient } from '@base44/sdk'; + +// Create a client with service role token +const base44 = createClient({ + appId: 'your-app-id', + token: 'user-token', // For user operations + serviceToken: 'service-token' // For service role operations +}); + +// User operations (uses user token) +const userEntities = await base44.entities.User.list(); + +// Service role operations (uses service token) +const allEntities = await base44.asServiceRole.entities.User.list(); + +// Service role has access to: +// - base44.asServiceRole.entities +// - base44.asServiceRole.integrations +// - base44.asServiceRole.functions +// Note: Service role does NOT have access to auth module for security + +// If no service token is provided, accessing asServiceRole throws an error +const clientWithoutService = createClient({ appId: 'your-app-id' }); +try { + await clientWithoutService.asServiceRole.entities.User.list(); +} catch (error) { + // Error: Service token is required to use asServiceRole +} +``` + +### Server-Side Usage + +For server-side applications, you can create a client from incoming HTTP requests: + +```javascript +import { createClientFromRequest } from '@base44/sdk'; + +// In your server handler (Express, Next.js, etc.) +app.get('/api/data', async (req, res) => { + try { + // Extract client configuration from request headers + const base44 = createClientFromRequest(req); + + // Headers used: + // - Authorization: Bearer + // - Base44-Service-Authorization: Bearer + // - Base44-App-Id: + // - Base44-Api-Url: (optional) + + // Use appropriate authentication based on available tokens + let data; + if (base44.asServiceRole) { + // Service token available - use elevated privileges + data = await base44.asServiceRole.entities.SensitiveData.list(); + } else { + // Only user token available - use user permissions + data = await base44.entities.PublicData.list(); + } + + res.json(data); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); +``` + ### Working with Integrations ```javascript @@ -103,7 +174,7 @@ import { getAccessToken } from '@base44/sdk/utils/auth-utils'; // Create a client with authentication const base44 = createClient({ appId: 'your-app-id', - accessToken: getAccessToken() // Automatically retrieves token from localStorage or URL + token: getAccessToken() // Automatically retrieves token from localStorage or URL }); // Check authentication status @@ -167,7 +238,7 @@ function AuthProvider({ children }) { const [client] = useState(() => createClient({ appId: 'your-app-id', - accessToken: getAccessToken() + token: getAccessToken() }) ); @@ -347,6 +418,24 @@ async function fetchProducts() { } } +// Service role operations with TypeScript +async function adminOperations() { + const base44 = createClient({ + appId: 'your-app-id', + serviceToken: 'service-token' + }); + + // TypeScript knows asServiceRole requires a service token + try { + const allUsers: Entity[] = await base44.asServiceRole.entities.User.list(); + console.log(`Total users: ${allUsers.length}`); + } catch (error) { + if (error instanceof Error) { + console.error(error.message); // Service token is required to use asServiceRole + } + } +} + // Authentication with TypeScript async function handleAuth(auth: AuthModule) { // Check authentication @@ -456,6 +545,25 @@ try { } ``` +## Functions + +The SDK supports invoking custom functions: + +```javascript +// Invoke a function without parameters +const result = await base44.functions.myFunction(); + +// Invoke a function with parameters +const result = await base44.functions.calculateTotal({ + items: ['item1', 'item2'], + discount: 0.1 +}); + +// Functions are automatically authenticated with the user token +// Service role can also invoke functions +const serviceResult = await base44.asServiceRole.functions.adminFunction(); +``` + ## Testing The SDK includes comprehensive tests to ensure reliability.