From 39c074799e68f316d7034e047abe8d3b4fe2e3fc Mon Sep 17 00:00:00 2001 From: Netanel Gilad Date: Thu, 14 Aug 2025 08:45:21 +0300 Subject: [PATCH 1/8] loginViaUsernamePassword -> loginViaEmailPassword --- src/modules/auth.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/auth.ts b/src/modules/auth.ts index c18bf2e..b5b6085 100644 --- a/src/modules/auth.ts +++ b/src/modules/auth.ts @@ -120,7 +120,7 @@ export function createAuthModule( * @param turnstileToken - Optional Turnstile captcha token * @returns Login response with access_token and user */ - async loginViaUsernamePassword( + async loginViaEmailPassword( email: string, password: string, turnstileToken?: string From 50596c38f3a7bbe8955a16b3144af21a64a80ee0 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 05:50:33 +0000 Subject: [PATCH 2/8] Fix test references: update loginViaUsernamePassword to loginViaEmailPassword Update all test method calls and describe block to use the new method name loginViaEmailPassword instead of loginViaUsernamePassword. This fixes the test suite that was broken after the method rename in src/modules/auth.ts. Co-authored-by: Netanel Gilad --- tests/unit/auth.test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/auth.test.js b/tests/unit/auth.test.js index e546fed..9f118e7 100644 --- a/tests/unit/auth.test.js +++ b/tests/unit/auth.test.js @@ -400,7 +400,7 @@ describe('Auth Module', () => { }); }); - describe('loginViaUsernamePassword()', () => { + describe('loginViaEmailPassword()', () => { test('should login successfully with email and password', async () => { const loginData = { email: 'test@example.com', @@ -421,7 +421,7 @@ describe('Auth Module', () => { .reply(200, mockResponse); // Call the API - const result = await base44.auth.loginViaUsernamePassword( + const result = await base44.auth.loginViaEmailPassword( loginData.email, loginData.password ); @@ -462,7 +462,7 @@ describe('Auth Module', () => { .reply(200, mockResponse); // Call the API - const result = await base44.auth.loginViaUsernamePassword( + const result = await base44.auth.loginViaEmailPassword( loginData.email, loginData.password, loginData.turnstile_token @@ -495,7 +495,7 @@ describe('Auth Module', () => { // Call the API and expect an error await expect( - base44.auth.loginViaUsernamePassword(loginData.email, loginData.password) + base44.auth.loginViaEmailPassword(loginData.email, loginData.password) ).rejects.toThrow(); // Verify all mocks were called @@ -514,7 +514,7 @@ describe('Auth Module', () => { // Call the API and expect an error await expect( - base44.auth.loginViaUsernamePassword(loginData.email, loginData.password) + base44.auth.loginViaEmailPassword(loginData.email, loginData.password) ).rejects.toThrow(); // Verify all mocks were called From f8c20663886cc4bf050b584ff97760dafe90d426 Mon Sep 17 00:00:00 2001 From: Netanel Gilad Date: Thu, 14 Aug 2025 11:52:53 +0300 Subject: [PATCH 3/8] Update claude.yml - opus --- .github/workflows/claude.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 659dac5..cc57bab 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -41,7 +41,7 @@ jobs: actions: read # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) - # model: "claude-opus-4-20250514" + model: "claude-opus-4-20250514" # Optional: Customize the trigger phrase (default: @claude) # trigger_phrase: "/claude" From 088c2a7b7d63de394fe41df5ca3381577cfeb065 Mon Sep 17 00:00:00 2001 From: Netanel Gilad Date: Thu, 14 Aug 2025 11:53:22 +0300 Subject: [PATCH 4/8] Update claude-code-review.yml - opus --- .github/workflows/claude-code-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index cd4147d..80bd49b 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -37,7 +37,7 @@ jobs: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) - # model: "claude-opus-4-20250514" + model: "claude-opus-4-20250514" # Direct prompt for automated review (no @claude mention needed) direct_prompt: | From 2e1008d85b27f3a9306c1656f5293cbefe8c5242 Mon Sep 17 00:00:00 2001 From: Netanel Gilad Date: Thu, 14 Aug 2025 13:02:41 +0300 Subject: [PATCH 5/8] better preview versions - real npm registry (#20) --- .github/workflows/main-publish.yml | 89 ++++++++++++++++- .github/workflows/preview-publish.yml | 133 ++++++++++++++++++++++---- 2 files changed, 203 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main-publish.yml b/.github/workflows/main-publish.yml index c0e785d..5c76617 100644 --- a/.github/workflows/main-publish.yml +++ b/.github/workflows/main-publish.yml @@ -20,6 +20,7 @@ jobs: with: node-version: 20 cache: "npm" + registry-url: "https://registry.npmjs.org" - name: Install dependencies run: npm install @@ -27,8 +28,92 @@ jobs: - name: Build package run: npm run build - - name: Publish main version with pkg.pr.new - run: npx pkg-pr-new publish --json output.json --comment=off + - name: Generate preview package name and version + id: preview_info + run: | + COMMIT_HASH="${{ github.sha }}" + SHORT_COMMIT="${COMMIT_HASH:0:7}" + + # Get current version from package.json + BASE_VERSION=$(node -p "require('./package.json').version") + + # Format: 0.3.0-dev.abc1234 (valid semver prerelease) + PREVIEW_VERSION="$BASE_VERSION-dev.$SHORT_COMMIT" + + echo "version=$PREVIEW_VERSION" >> $GITHUB_OUTPUT + echo "package_name=@base44-preview/sdk" >> $GITHUB_OUTPUT + echo "full_package=@base44-preview/sdk@$PREVIEW_VERSION" >> $GITHUB_OUTPUT + + - name: Update package.json for preview + run: | + # Create a backup of original package.json + cp package.json package.json.bak + + # Get the official package name for safety checks + OFFICIAL_PACKAGE=$(node -p "require('./package.json').name") + PREVIEW_PACKAGE="${{ steps.preview_info.outputs.package_name }}" + + echo "Official package: $OFFICIAL_PACKAGE" + echo "Preview package: $PREVIEW_PACKAGE" + + # Safety check: Ensure we're not accidentally using the official package name + if [ "$PREVIEW_PACKAGE" = "$OFFICIAL_PACKAGE" ]; then + echo "❌ ERROR: Preview package name matches official package name!" + echo "This would overwrite the official package. Aborting." + exit 1 + fi + + # Update name with error handling + if ! npm pkg set name="$PREVIEW_PACKAGE"; then + echo "❌ ERROR: Failed to set package name to $PREVIEW_PACKAGE" + exit 1 + fi + + # Update version with error handling + if ! npm pkg set version="${{ steps.preview_info.outputs.version }}"; then + echo "❌ ERROR: Failed to set package version to ${{ steps.preview_info.outputs.version }}" + exit 1 + fi + + echo "✅ Package.json updated successfully" + + - name: Final safety check before publish + run: | + # Double-check package name one more time before publishing + CURRENT_PACKAGE_NAME=$(node -p "require('./package.json').name") + OFFICIAL_PACKAGE=$(jq -r '.name' package.json.bak) + + echo "About to publish: $CURRENT_PACKAGE_NAME" + + if [ "$CURRENT_PACKAGE_NAME" = "$OFFICIAL_PACKAGE" ]; then + echo "❌ CRITICAL ERROR: About to publish to official package name!" + echo "This is not allowed. Check the workflow configuration." + exit 1 + fi + + echo "✅ Safety check passed. Package name is safe to publish." + + - name: Publish preview package + run: | + if npm publish --tag preview; then + echo "✅ Package published successfully" + else + echo "❌ Package publish failed" + exit 1 + fi + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Restore original package.json + if: always() + run: | + if [ -f package.json.bak ]; then + mv package.json.bak package.json + echo "✅ Original package.json restored" + else + echo "❌ WARNING: Backup file package.json.bak not found" + echo "This could indicate an earlier step failed" + fi permissions: contents: read diff --git a/.github/workflows/preview-publish.yml b/.github/workflows/preview-publish.yml index 1990590..d30f37f 100644 --- a/.github/workflows/preview-publish.yml +++ b/.github/workflows/preview-publish.yml @@ -20,6 +20,7 @@ jobs: with: node-version: 20 cache: "npm" + registry-url: "https://registry.npmjs.org" - name: Install dependencies run: npm install @@ -27,25 +28,108 @@ jobs: - name: Build package run: npm run build - - name: Publish preview with pkg.pr.new - run: npx pkg-pr-new publish --json output.json --comment=off + - name: Generate preview package name and version + id: preview_info + run: | + PR_NUMBER="${{ github.event.number }}" + COMMIT_HASH="${{ github.sha }}" + SHORT_COMMIT="${COMMIT_HASH:0:7}" + + # Get current version from package.json + BASE_VERSION=$(node -p "require('./package.json').version") + + if [ ! -z "$PR_NUMBER" ]; then + # Format: 0.3.0-pr.123.abc1234 (valid semver prerelease) + PREVIEW_VERSION="$BASE_VERSION-pr.$PR_NUMBER.$SHORT_COMMIT" + else + # Format: 0.3.0-dev.abc1234 (valid semver prerelease) + PREVIEW_VERSION="$BASE_VERSION-dev.$SHORT_COMMIT" + fi + + echo "version=$PREVIEW_VERSION" >> $GITHUB_OUTPUT + echo "package_name=@base44-preview/sdk" >> $GITHUB_OUTPUT + echo "full_package=@base44-preview/sdk@$PREVIEW_VERSION" >> $GITHUB_OUTPUT + + - name: Update package.json for preview + run: | + # Create a backup of original package.json + cp package.json package.json.bak + + # Get the official package name for safety checks + OFFICIAL_PACKAGE=$(node -p "require('./package.json').name") + PREVIEW_PACKAGE="${{ steps.preview_info.outputs.package_name }}" + + echo "Official package: $OFFICIAL_PACKAGE" + echo "Preview package: $PREVIEW_PACKAGE" + + # Safety check: Ensure we're not accidentally using the official package name + if [ "$PREVIEW_PACKAGE" = "$OFFICIAL_PACKAGE" ]; then + echo "❌ ERROR: Preview package name matches official package name!" + echo "This would overwrite the official package. Aborting." + exit 1 + fi + + # Update name with error handling + if ! npm pkg set name="$PREVIEW_PACKAGE"; then + echo "❌ ERROR: Failed to set package name to $PREVIEW_PACKAGE" + exit 1 + fi + + # Update version with error handling + if ! npm pkg set version="${{ steps.preview_info.outputs.version }}"; then + echo "❌ ERROR: Failed to set package version to ${{ steps.preview_info.outputs.version }}" + exit 1 + fi + + echo "✅ Package.json updated successfully" + + - name: Final safety check before publish + run: | + # Double-check package name one more time before publishing + CURRENT_PACKAGE_NAME=$(node -p "require('./package.json').name") + OFFICIAL_PACKAGE=$(jq -r '.name' package.json.bak) + + echo "About to publish: $CURRENT_PACKAGE_NAME" + + if [ "$CURRENT_PACKAGE_NAME" = "$OFFICIAL_PACKAGE" ]; then + echo "❌ CRITICAL ERROR: About to publish to official package name!" + echo "This is not allowed. Check the workflow configuration." + exit 1 + fi + + echo "✅ Safety check passed. Package name is safe to publish." + + - name: Publish preview package + run: | + if npm publish --tag preview; then + echo "✅ Package published successfully" + else + echo "❌ Package publish failed" + exit 1 + fi + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Restore original package.json + if: always() + run: | + if [ -f package.json.bak ]; then + mv package.json.bak package.json + echo "✅ Original package.json restored" + else + echo "❌ WARNING: Backup file package.json.bak not found" + echo "This could indicate an earlier step failed" + fi - name: Comment PR with install instructions uses: actions/github-script@v6 with: script: | - const fs = require('fs'); - const output = JSON.parse(fs.readFileSync('output.json', 'utf8')); - if (!output.packages || output.packages.length === 0) { - core.setFailed('No packages published by pkg.pr.new'); - return; - } - const pkg = output.packages[0]; - const installCmd = `npm i ${pkg.url}`; - const badge = `[![pkg.pr.new](https://pkg.pr.new/badge/${context.repo.owner}/${context.repo.repo})](https://pkg.pr.new/~/${context.repo.owner}/${context.repo.repo})`; - const body = `### 🚀 Package Preview Available! + const fullPackage = '${{ steps.preview_info.outputs.full_package }}'; + const installCmd = `npm i ${fullPackage}`; + const aliasInstallCmd = `npm i "@base44/sdk@npm:${fullPackage}"`; - ${badge} + const body = `### 🚀 Package Preview Available! --- @@ -55,12 +139,27 @@ jobs: ${installCmd} \`\`\` - - 📦 [Preview Package on pkg.pr.new](${pkg.url}) - - 🔗 [View this commit on GitHub](https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${pkg.commit}) + **Prefer not to change any import paths? Install using npm alias so your code still imports \`@base44/sdk\`:** + + \`\`\`sh + ${aliasInstallCmd} + \`\`\` + + Or add it to your \`package.json\` dependencies: + + \`\`\`json + { + "dependencies": { + "@base44/sdk": "npm:${fullPackage}" + } + } + \`\`\` + + - 📦 **Preview Package**: \`${fullPackage}\` + - 🔗 [View this commit on GitHub](https://github.com/${{ github.repository }}/commit/${{ github.sha }}) --- - Preview powered by [pkg.pr.new](https://pkg.pr.new) — try new features instantly, no NPM release needed! - `; + Preview published to npm registry — try new features instantly!`; const botCommentIdentifier = '### 🚀 Package Preview Available!'; From aaff18b639e5073cfe71798931f416fabb502276 Mon Sep 17 00:00:00 2001 From: Netanel Gilad Date: Thu, 14 Aug 2025 13:17:43 +0300 Subject: [PATCH 6/8] claude won't review itself --- .github/workflows/claude-code-review.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 80bd49b..ed5bcb4 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -13,8 +13,10 @@ on: jobs: claude-review: if: | - !contains(github.event.pull_request.head.commit.author.name, 'Claude') && - !contains(github.event.pull_request.head.commit.author.email, 'noreply@anthropic.com') && + github.actor != 'github-actions[bot]' && + github.actor != 'claude-code[bot]' && + github.actor != 'claude[bot]' && + github.actor != 'claude' && github.event.pull_request.head.commit.parents[1] == null runs-on: ubuntu-latest From 64bdeca3365907ea0199f80aedb27338aebcb3df Mon Sep 17 00:00:00 2001 From: Netanel Gilad Date: Thu, 14 Aug 2025 14:48:41 +0300 Subject: [PATCH 7/8] service role support (#19) --- README.md | 120 ++++++++++- src/client.ts | 124 ++++++++--- src/index.ts | 4 +- src/modules/auth.ts | 7 +- tests/unit/client.test.js | 389 ++++++++++++++++++++++++++++++++++- tests/unit/functions.test.ts | 35 ++++ 6 files changed, 639 insertions(+), 40 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. diff --git a/src/client.ts b/src/client.ts index d60687f..f8061db 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,27 @@ 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, + const client = { + ...userModules, /** * Set authentication token for all requests * @param {string} newToken - New auth token */ setToken(newToken: string) { - auth.setToken(newToken); + userModules.auth.setToken(newToken); }, /** @@ -110,9 +132,61 @@ export function createClient(config: { return { serverUrl, appId, - env, 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) { + 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" + ); + } + + // 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", + 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 b5b6085..bdc9950 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 ( diff --git a/tests/unit/client.test.js b/tests/unit/client.test.js index 73b0572..f1a0c5c 100644 --- a/tests/unit/client.test.js +++ b/tests/unit/client.test.js @@ -1,5 +1,6 @@ -import { createClient } from '../../src/index.ts'; -import { describe, test, expect } from 'vitest'; +import { createClient, createClientFromRequest } from '../../src/index.ts'; +import { describe, test, expect, beforeEach, afterEach } from 'vitest'; +import nock from 'nock'; describe('Client Creation', () => { test('should create a client with default options', () => { @@ -15,15 +16,16 @@ 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); + + // 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', () => { const client = createClient({ appId: 'test-app-id', serverUrl: 'https://custom-server.com', - env: 'dev', requiresAuth: true, token: 'test-token', }); @@ -33,7 +35,384 @@ 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); }); + + 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(); + // 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 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 throw error for malformed authorization headers', () => { + const mockRequest = { + headers: { + get: (name) => { + const headers = { + 'Authorization': 'InvalidFormat', + 'Base44-Service-Authorization': 'AlsoInvalid', + 'Base44-App-Id': 'test-app-id' + }; + return headers[name] || null; + } + } + }; + + // Should throw error for malformed headers instead of continuing silently + expect(() => createClientFromRequest(mockRequest)).toThrow('Invalid authorization header format. Expected "Bearer "'); + }); + + test('should throw error for empty authorization headers', () => { + const mockRequest = { + headers: { + get: (name) => { + const headers = { + 'Authorization': '', + 'Base44-Service-Authorization': '', + 'Base44-App-Id': 'test-app-id' + }; + return headers[name] === '' ? '' : headers[name] || null; + } + } + }; + + // Should throw error for empty headers instead of continuing silently + expect(() => createClientFromRequest(mockRequest)).toThrow('Invalid authorization header format. Expected "Bearer "'); + }); +}); + + +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); + }); + }); \ No newline at end of file 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 425ba6a7e96994da606b7a1fffe4ff12d8051172 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 14 Aug 2025 11:51:58 +0000 Subject: [PATCH 8/8] chore: bump version to 0.4.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 67ef850..c2606b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@base44/sdk", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@base44/sdk", - "version": "0.3.0", + "version": "0.4.0", "license": "MIT", "dependencies": { "axios": "^1.6.2" diff --git a/package.json b/package.json index aadc387..1949177 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@base44/sdk", - "version": "0.3.0", + "version": "0.4.0", "description": "JavaScript SDK for Base44 API", "main": "dist/index.js", "types": "dist/index.d.ts",