@base44-preview/sdk 0.0.0-preview → 0.3.0-dev.485c22b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +634 -0
- package/dist/client.d.ts +50 -3
- package/dist/client.js +83 -24
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -4
- package/dist/modules/auth.d.ts +1 -1
- package/dist/modules/auth.js +2 -1
- package/package.json +18 -7
package/README.md
ADDED
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
# Base44 JavaScript SDK
|
|
2
|
+
|
|
3
|
+
A modern JavaScript SDK for interacting with the Base44 API. Designed to work with both JavaScript and TypeScript projects.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @base44/sdk
|
|
9
|
+
# or
|
|
10
|
+
yarn add @base44/sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Basic Setup
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
import { createClient } from '@base44/sdk';
|
|
19
|
+
|
|
20
|
+
// Create a client instance
|
|
21
|
+
const base44 = createClient({
|
|
22
|
+
serverUrl: 'https://base44.app', // Optional, defaults to 'https://base44.app'
|
|
23
|
+
appId: 'your-app-id', // Required
|
|
24
|
+
token: 'your-user-token', // Optional, for user authentication
|
|
25
|
+
serviceToken: 'your-service-token', // Optional, for service role authentication
|
|
26
|
+
autoInitAuth: true, // Optional, defaults to true - auto-detects tokens from URL or localStorage
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Working with Entities
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
// List all products
|
|
34
|
+
const products = await base44.entities.Product.list();
|
|
35
|
+
|
|
36
|
+
// Filter products by category
|
|
37
|
+
const filteredProducts = await base44.entities.Product.filter({
|
|
38
|
+
category: ['electronics', 'computers']
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Get a specific product
|
|
42
|
+
const product = await base44.entities.Product.get('product-id');
|
|
43
|
+
|
|
44
|
+
// Create a new product
|
|
45
|
+
const newProduct = await base44.entities.Product.create({
|
|
46
|
+
name: 'New Product',
|
|
47
|
+
price: 99.99,
|
|
48
|
+
category: 'electronics'
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Update a product
|
|
52
|
+
const updatedProduct = await base44.entities.Product.update('product-id', {
|
|
53
|
+
price: 89.99
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Delete a product
|
|
57
|
+
await base44.entities.Product.delete('product-id');
|
|
58
|
+
|
|
59
|
+
// Bulk create products
|
|
60
|
+
const newProducts = await base44.entities.Product.bulkCreate([
|
|
61
|
+
{ name: 'Product 1', price: 19.99 },
|
|
62
|
+
{ name: 'Product 2', price: 29.99 }
|
|
63
|
+
]);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Service Role Authentication
|
|
67
|
+
|
|
68
|
+
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.
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
import { createClient } from '@base44/sdk';
|
|
72
|
+
|
|
73
|
+
// Create a client with service role token
|
|
74
|
+
const base44 = createClient({
|
|
75
|
+
appId: 'your-app-id',
|
|
76
|
+
token: 'user-token', // For user operations
|
|
77
|
+
serviceToken: 'service-token' // For service role operations
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// User operations (uses user token)
|
|
81
|
+
const userEntities = await base44.entities.User.list();
|
|
82
|
+
|
|
83
|
+
// Service role operations (uses service token)
|
|
84
|
+
const allEntities = await base44.asServiceRole.entities.User.list();
|
|
85
|
+
|
|
86
|
+
// Service role has access to:
|
|
87
|
+
// - base44.asServiceRole.entities
|
|
88
|
+
// - base44.asServiceRole.integrations
|
|
89
|
+
// - base44.asServiceRole.functions
|
|
90
|
+
// Note: Service role does NOT have access to auth module for security
|
|
91
|
+
|
|
92
|
+
// If no service token is provided, accessing asServiceRole throws an error
|
|
93
|
+
const clientWithoutService = createClient({ appId: 'your-app-id' });
|
|
94
|
+
try {
|
|
95
|
+
await clientWithoutService.asServiceRole.entities.User.list();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// Error: Service token is required to use asServiceRole
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Server-Side Usage
|
|
102
|
+
|
|
103
|
+
For server-side applications, you can create a client from incoming HTTP requests:
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
import { createClientFromRequest } from '@base44/sdk';
|
|
107
|
+
|
|
108
|
+
// In your server handler (Express, Next.js, etc.)
|
|
109
|
+
app.get('/api/data', async (req, res) => {
|
|
110
|
+
try {
|
|
111
|
+
// Extract client configuration from request headers
|
|
112
|
+
const base44 = createClientFromRequest(req);
|
|
113
|
+
|
|
114
|
+
// Headers used:
|
|
115
|
+
// - Authorization: Bearer <user-token>
|
|
116
|
+
// - Base44-Service-Authorization: Bearer <service-token>
|
|
117
|
+
// - Base44-App-Id: <app-id>
|
|
118
|
+
// - Base44-Api-Url: <custom-api-url> (optional)
|
|
119
|
+
|
|
120
|
+
// Use appropriate authentication based on available tokens
|
|
121
|
+
let data;
|
|
122
|
+
if (base44.asServiceRole) {
|
|
123
|
+
// Service token available - use elevated privileges
|
|
124
|
+
data = await base44.asServiceRole.entities.SensitiveData.list();
|
|
125
|
+
} else {
|
|
126
|
+
// Only user token available - use user permissions
|
|
127
|
+
data = await base44.entities.PublicData.list();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
res.json(data);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
res.status(500).json({ error: error.message });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Working with Integrations
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
// Send an email using the Core integration
|
|
141
|
+
const emailResult = await base44.integrations.Core.SendEmail({
|
|
142
|
+
to: 'user@example.com',
|
|
143
|
+
subject: 'Hello from Base44',
|
|
144
|
+
body: 'This is a test email sent via the Base44 SDK'
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Use a custom integration
|
|
148
|
+
const result = await base44.integrations.CustomPackage.CustomEndpoint({
|
|
149
|
+
param1: 'value1',
|
|
150
|
+
param2: 'value2'
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Upload a file
|
|
154
|
+
const fileInput = document.querySelector('input[type="file"]');
|
|
155
|
+
const file = fileInput.files[0];
|
|
156
|
+
const uploadResult = await base44.integrations.Core.UploadFile({
|
|
157
|
+
file,
|
|
158
|
+
metadata: { type: 'profile-picture' }
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Authentication
|
|
163
|
+
|
|
164
|
+
The SDK provides comprehensive authentication capabilities to help you build secure applications.
|
|
165
|
+
|
|
166
|
+
### Creating an Authenticated Client
|
|
167
|
+
|
|
168
|
+
To create a client with authentication:
|
|
169
|
+
|
|
170
|
+
```javascript
|
|
171
|
+
import { createClient } from '@base44/sdk';
|
|
172
|
+
import { getAccessToken } from '@base44/sdk/utils/auth-utils';
|
|
173
|
+
|
|
174
|
+
// Create a client with authentication
|
|
175
|
+
const base44 = createClient({
|
|
176
|
+
appId: 'your-app-id',
|
|
177
|
+
token: getAccessToken() // Automatically retrieves token from localStorage or URL
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Check authentication status
|
|
181
|
+
const isAuthenticated = await base44.auth.isAuthenticated();
|
|
182
|
+
console.log('Authenticated:', isAuthenticated);
|
|
183
|
+
|
|
184
|
+
// Get current user information (requires authentication)
|
|
185
|
+
if (isAuthenticated) {
|
|
186
|
+
const user = await base44.auth.me();
|
|
187
|
+
console.log('Current user:', user);
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Login and Logout
|
|
192
|
+
|
|
193
|
+
```javascript
|
|
194
|
+
import { createClient } from '@base44/sdk';
|
|
195
|
+
import { getAccessToken, saveAccessToken, removeAccessToken } from '@base44/sdk/utils/auth-utils';
|
|
196
|
+
|
|
197
|
+
const base44 = createClient({ appId: 'your-app-id' });
|
|
198
|
+
|
|
199
|
+
// Redirect to the login page
|
|
200
|
+
// This will redirect to: base44.app/login?from_url=http://your-app.com/dashboard&app_id=your-app-id
|
|
201
|
+
function handleLogin() {
|
|
202
|
+
base44.auth.login('/dashboard');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Handle successful login (on return from Base44 login)
|
|
206
|
+
function handleLoginReturn() {
|
|
207
|
+
const token = getAccessToken();
|
|
208
|
+
if (token) {
|
|
209
|
+
console.log('Successfully logged in with token:', token);
|
|
210
|
+
// The token is automatically saved to localStorage and removed from URL
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Logout
|
|
215
|
+
function handleLogout() {
|
|
216
|
+
removeAccessToken();
|
|
217
|
+
window.location.href = '/login';
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Real-World Authentication Example (React)
|
|
222
|
+
|
|
223
|
+
Here's a complete example of implementing Base44 authentication in a React application:
|
|
224
|
+
|
|
225
|
+
```jsx
|
|
226
|
+
import React, { createContext, useContext, useEffect, useState } from 'react';
|
|
227
|
+
import { Navigate, Outlet, Route, Routes, useLocation } from 'react-router-dom';
|
|
228
|
+
import { createClient } from '@base44/sdk';
|
|
229
|
+
import { getAccessToken, removeAccessToken } from '@base44/sdk/utils/auth-utils';
|
|
230
|
+
|
|
231
|
+
// Create AuthContext
|
|
232
|
+
const AuthContext = createContext(null);
|
|
233
|
+
|
|
234
|
+
// Auth Provider Component
|
|
235
|
+
function AuthProvider({ children }) {
|
|
236
|
+
const [user, setUser] = useState(null);
|
|
237
|
+
const [loading, setLoading] = useState(true);
|
|
238
|
+
const [client] = useState(() =>
|
|
239
|
+
createClient({
|
|
240
|
+
appId: 'your-app-id',
|
|
241
|
+
token: getAccessToken()
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
async function loadUser() {
|
|
247
|
+
try {
|
|
248
|
+
const isAuth = await client.auth.isAuthenticated();
|
|
249
|
+
if (isAuth) {
|
|
250
|
+
const userData = await client.auth.me();
|
|
251
|
+
setUser(userData);
|
|
252
|
+
}
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.error('Authentication error:', error);
|
|
255
|
+
} finally {
|
|
256
|
+
setLoading(false);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
loadUser();
|
|
261
|
+
}, [client]);
|
|
262
|
+
|
|
263
|
+
const login = () => {
|
|
264
|
+
client.auth.login(window.location.pathname);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const logout = () => {
|
|
268
|
+
removeAccessToken();
|
|
269
|
+
setUser(null);
|
|
270
|
+
window.location.href = '/login';
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<AuthContext.Provider value={{ user, loading, client, login, logout }}>
|
|
275
|
+
{children}
|
|
276
|
+
</AuthContext.Provider>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Custom hook to use auth context
|
|
281
|
+
function useAuth() {
|
|
282
|
+
const context = useContext(AuthContext);
|
|
283
|
+
if (!context) {
|
|
284
|
+
throw new Error('useAuth must be used within an AuthProvider');
|
|
285
|
+
}
|
|
286
|
+
return context;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Protected Route Component
|
|
290
|
+
function ProtectedRoute() {
|
|
291
|
+
const { user, loading, login } = useAuth();
|
|
292
|
+
const location = useLocation();
|
|
293
|
+
|
|
294
|
+
// Check if we're returning from login with a token in URL
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
const token = getAccessToken(); // This will save token from URL if present
|
|
297
|
+
if (token && !user && !loading) {
|
|
298
|
+
window.location.reload(); // Reload to apply the new token
|
|
299
|
+
}
|
|
300
|
+
}, [location, user, loading]);
|
|
301
|
+
|
|
302
|
+
if (loading) {
|
|
303
|
+
return <div>Loading...</div>;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!user) {
|
|
307
|
+
// If not authenticated, redirect to login
|
|
308
|
+
login();
|
|
309
|
+
return <div>Redirecting to login...</div>;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// If authenticated, render the child routes
|
|
313
|
+
return <Outlet />;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Dashboard Component (protected)
|
|
317
|
+
function Dashboard() {
|
|
318
|
+
const { user, client, logout } = useAuth();
|
|
319
|
+
const [todos, setTodos] = useState([]);
|
|
320
|
+
const [loading, setLoading] = useState(true);
|
|
321
|
+
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
async function loadTodos() {
|
|
324
|
+
try {
|
|
325
|
+
// Load user-specific data using the SDK
|
|
326
|
+
const items = await client.entities.Todo.filter({
|
|
327
|
+
assignee: user.id
|
|
328
|
+
});
|
|
329
|
+
setTodos(items);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error('Failed to load todos:', error);
|
|
332
|
+
} finally {
|
|
333
|
+
setLoading(false);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
loadTodos();
|
|
338
|
+
}, [client, user]);
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<div>
|
|
342
|
+
<h1>Welcome, {user.name}!</h1>
|
|
343
|
+
<button onClick={logout}>Logout</button>
|
|
344
|
+
|
|
345
|
+
<h2>Your Todos</h2>
|
|
346
|
+
{loading ? (
|
|
347
|
+
<div>Loading todos...</div>
|
|
348
|
+
) : (
|
|
349
|
+
<ul>
|
|
350
|
+
{todos.map(todo => (
|
|
351
|
+
<li key={todo.id}>{todo.title}</li>
|
|
352
|
+
))}
|
|
353
|
+
</ul>
|
|
354
|
+
)}
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Login Page
|
|
360
|
+
function LoginPage() {
|
|
361
|
+
const { login, user } = useAuth();
|
|
362
|
+
|
|
363
|
+
if (user) {
|
|
364
|
+
return <Navigate to="/dashboard" />;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return (
|
|
368
|
+
<div>
|
|
369
|
+
<h1>Login Required</h1>
|
|
370
|
+
<button onClick={login}>Login with Base44</button>
|
|
371
|
+
</div>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// App Component
|
|
376
|
+
function App() {
|
|
377
|
+
return (
|
|
378
|
+
<AuthProvider>
|
|
379
|
+
<Routes>
|
|
380
|
+
<Route path="/login" element={<LoginPage />} />
|
|
381
|
+
<Route element={<ProtectedRoute />}>
|
|
382
|
+
<Route path="/dashboard" element={<Dashboard />} />
|
|
383
|
+
<Route path="/profile" element={<ProfilePage />} />
|
|
384
|
+
{/* Add more protected routes here */}
|
|
385
|
+
</Route>
|
|
386
|
+
<Route path="/" element={<Navigate to="/dashboard" />} />
|
|
387
|
+
</Routes>
|
|
388
|
+
</AuthProvider>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## TypeScript Support
|
|
394
|
+
|
|
395
|
+
This SDK includes TypeScript definitions out of the box:
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
import { createClient, Base44Error } from '@base44/sdk';
|
|
399
|
+
import type { Entity, Base44Client, AuthModule } from '@base44/sdk';
|
|
400
|
+
|
|
401
|
+
// Create a typed client
|
|
402
|
+
const base44: Base44Client = createClient({
|
|
403
|
+
appId: 'your-app-id'
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Using the entities module with type safety
|
|
407
|
+
async function fetchProducts() {
|
|
408
|
+
try {
|
|
409
|
+
const products: Entity[] = await base44.entities.Product.list();
|
|
410
|
+
console.log(products.map(p => p.name));
|
|
411
|
+
|
|
412
|
+
const product: Entity = await base44.entities.Product.get('product-id');
|
|
413
|
+
console.log(product.name);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (error instanceof Base44Error) {
|
|
416
|
+
console.error(`Error ${error.status}: ${error.message}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Service role operations with TypeScript
|
|
422
|
+
async function adminOperations() {
|
|
423
|
+
const base44 = createClient({
|
|
424
|
+
appId: 'your-app-id',
|
|
425
|
+
serviceToken: 'service-token'
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// TypeScript knows asServiceRole requires a service token
|
|
429
|
+
try {
|
|
430
|
+
const allUsers: Entity[] = await base44.asServiceRole.entities.User.list();
|
|
431
|
+
console.log(`Total users: ${allUsers.length}`);
|
|
432
|
+
} catch (error) {
|
|
433
|
+
if (error instanceof Error) {
|
|
434
|
+
console.error(error.message); // Service token is required to use asServiceRole
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Authentication with TypeScript
|
|
440
|
+
async function handleAuth(auth: AuthModule) {
|
|
441
|
+
// Check authentication
|
|
442
|
+
const isAuthenticated: boolean = await auth.isAuthenticated();
|
|
443
|
+
|
|
444
|
+
if (isAuthenticated) {
|
|
445
|
+
// Get user info
|
|
446
|
+
const user: Entity = await auth.me();
|
|
447
|
+
console.log(`Logged in as: ${user.name}, Role: ${user.role}`);
|
|
448
|
+
|
|
449
|
+
// Update user
|
|
450
|
+
const updatedUser: Entity = await auth.updateMe({
|
|
451
|
+
preferences: { theme: 'dark' }
|
|
452
|
+
});
|
|
453
|
+
} else {
|
|
454
|
+
// Redirect to login
|
|
455
|
+
auth.login('/dashboard');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Execute with proper typing
|
|
460
|
+
handleAuth(base44.auth);
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### Advanced TypeScript Usage
|
|
464
|
+
|
|
465
|
+
You can define your own entity interfaces for better type safety:
|
|
466
|
+
|
|
467
|
+
```typescript
|
|
468
|
+
// Define custom entity interfaces
|
|
469
|
+
interface User extends Entity {
|
|
470
|
+
name: string;
|
|
471
|
+
email: string;
|
|
472
|
+
role: 'admin' | 'editor' | 'viewer';
|
|
473
|
+
preferences?: {
|
|
474
|
+
theme: 'light' | 'dark';
|
|
475
|
+
notifications: boolean;
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
interface Product extends Entity {
|
|
480
|
+
name: string;
|
|
481
|
+
price: number;
|
|
482
|
+
category: string;
|
|
483
|
+
inStock: boolean;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Use your custom interfaces with the SDK
|
|
487
|
+
async function getLoggedInUser(): Promise<User | null> {
|
|
488
|
+
const base44 = createClient({ appId: 'your-app-id' });
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
const user = await base44.auth.me() as User;
|
|
492
|
+
return user;
|
|
493
|
+
} catch (error) {
|
|
494
|
+
console.error('Failed to get user:', error);
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// Use with React hooks
|
|
500
|
+
function useBase44User() {
|
|
501
|
+
const [user, setUser] = useState<User | null>(null);
|
|
502
|
+
const [loading, setLoading] = useState<boolean>(true);
|
|
503
|
+
|
|
504
|
+
useEffect(() => {
|
|
505
|
+
const base44 = createClient({ appId: 'your-app-id' });
|
|
506
|
+
|
|
507
|
+
async function fetchUser() {
|
|
508
|
+
try {
|
|
509
|
+
const userData = await base44.auth.me() as User;
|
|
510
|
+
setUser(userData);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
console.error('Auth error:', error);
|
|
513
|
+
} finally {
|
|
514
|
+
setLoading(false);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
fetchUser();
|
|
519
|
+
}, []);
|
|
520
|
+
|
|
521
|
+
return { user, loading };
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## Error Handling
|
|
526
|
+
|
|
527
|
+
The SDK provides a custom `Base44Error` class for error handling:
|
|
528
|
+
|
|
529
|
+
```javascript
|
|
530
|
+
import { createClient, Base44Error } from '@base44/sdk';
|
|
531
|
+
|
|
532
|
+
const base44 = createClient({ appId: 'your-app-id' });
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
const result = await base44.entities.NonExistentEntity.list();
|
|
536
|
+
} catch (error) {
|
|
537
|
+
if (error instanceof Base44Error) {
|
|
538
|
+
console.error(`Status: ${error.status}`);
|
|
539
|
+
console.error(`Message: ${error.message}`);
|
|
540
|
+
console.error(`Code: ${error.code}`);
|
|
541
|
+
console.error(`Data: ${JSON.stringify(error.data)}`);
|
|
542
|
+
} else {
|
|
543
|
+
console.error('Unexpected error:', error);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
## Functions
|
|
549
|
+
|
|
550
|
+
The SDK supports invoking custom functions:
|
|
551
|
+
|
|
552
|
+
```javascript
|
|
553
|
+
// Invoke a function without parameters
|
|
554
|
+
const result = await base44.functions.myFunction();
|
|
555
|
+
|
|
556
|
+
// Invoke a function with parameters
|
|
557
|
+
const result = await base44.functions.calculateTotal({
|
|
558
|
+
items: ['item1', 'item2'],
|
|
559
|
+
discount: 0.1
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// Functions are automatically authenticated with the user token
|
|
563
|
+
// Service role can also invoke functions
|
|
564
|
+
const serviceResult = await base44.asServiceRole.functions.adminFunction();
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
## Testing
|
|
568
|
+
|
|
569
|
+
The SDK includes comprehensive tests to ensure reliability.
|
|
570
|
+
|
|
571
|
+
### Running Tests
|
|
572
|
+
|
|
573
|
+
```bash
|
|
574
|
+
# Run all tests
|
|
575
|
+
npm test
|
|
576
|
+
|
|
577
|
+
# Run unit tests only (no API calls)
|
|
578
|
+
npm run test:unit
|
|
579
|
+
|
|
580
|
+
# Run end-to-end tests (requires API access)
|
|
581
|
+
npm run test:e2e
|
|
582
|
+
|
|
583
|
+
# Run tests with coverage report
|
|
584
|
+
npm run test:coverage
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Setting Up E2E Tests
|
|
588
|
+
|
|
589
|
+
E2E tests require access to a Base44 API. To run these tests:
|
|
590
|
+
|
|
591
|
+
1. Copy `tests/.env.example` to `tests/.env`
|
|
592
|
+
2. Fill in your Base44 API credentials in the `.env` file:
|
|
593
|
+
```
|
|
594
|
+
BASE44_SERVER_URL=https://base44.app
|
|
595
|
+
BASE44_APP_ID=your_app_id_here
|
|
596
|
+
BASE44_AUTH_TOKEN=your_auth_token_here
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
3. Optionally, set `SKIP_E2E_TESTS=true` to skip E2E tests.
|
|
600
|
+
|
|
601
|
+
### Writing Your Own Tests
|
|
602
|
+
|
|
603
|
+
You can use the provided test utilities for writing your own tests:
|
|
604
|
+
|
|
605
|
+
```javascript
|
|
606
|
+
const { createClient } = require('@base44/sdk');
|
|
607
|
+
const { getTestConfig } = require('@base44/sdk/tests/utils/test-config');
|
|
608
|
+
|
|
609
|
+
describe('My Tests', () => {
|
|
610
|
+
let base44;
|
|
611
|
+
|
|
612
|
+
beforeAll(() => {
|
|
613
|
+
const config = getTestConfig();
|
|
614
|
+
base44 = createClient({
|
|
615
|
+
serverUrl: config.serverUrl,
|
|
616
|
+
appId: config.appId,
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
if (config.token) {
|
|
620
|
+
base44.setToken(config.token);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test('My test', async () => {
|
|
625
|
+
const todos = await base44.entities.Todo.filter({}, 10);
|
|
626
|
+
expect(Array.isArray(todos)).toBe(true);
|
|
627
|
+
expect(todos.length).toBeGreaterThan(0);
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
## License
|
|
633
|
+
|
|
634
|
+
MIT
|
package/dist/client.d.ts
CHANGED
|
@@ -3,18 +3,39 @@
|
|
|
3
3
|
* @param {Object} config - Client configuration
|
|
4
4
|
* @param {string} [config.serverUrl='https://base44.app'] - API server URL
|
|
5
5
|
* @param {string|number} config.appId - Application ID
|
|
6
|
-
* @param {string} [config.env='prod'] - Environment ('prod' or 'dev')
|
|
7
6
|
* @param {string} [config.token] - Authentication token
|
|
7
|
+
* @param {string} [config.serviceToken] - Service role authentication token
|
|
8
8
|
* @param {boolean} [config.requiresAuth=false] - Whether the app requires authentication
|
|
9
9
|
* @returns {Object} Base44 client instance
|
|
10
10
|
*/
|
|
11
11
|
export declare function createClient(config: {
|
|
12
12
|
serverUrl?: string;
|
|
13
13
|
appId: string;
|
|
14
|
-
env?: string;
|
|
15
14
|
token?: string;
|
|
15
|
+
serviceToken?: string;
|
|
16
16
|
requiresAuth?: boolean;
|
|
17
17
|
}): {
|
|
18
|
+
/**
|
|
19
|
+
* Set authentication token for all requests
|
|
20
|
+
* @param {string} newToken - New auth token
|
|
21
|
+
*/
|
|
22
|
+
setToken(newToken: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Get current configuration
|
|
25
|
+
* @returns {Object} Current configuration
|
|
26
|
+
*/
|
|
27
|
+
getConfig(): {
|
|
28
|
+
serverUrl: string;
|
|
29
|
+
appId: string;
|
|
30
|
+
requiresAuth: boolean;
|
|
31
|
+
};
|
|
32
|
+
asServiceRole: {
|
|
33
|
+
entities: {};
|
|
34
|
+
integrations: {};
|
|
35
|
+
functions: {
|
|
36
|
+
invoke(functionName: string, data: Record<string, any>): Promise<import("axios").AxiosResponse<any, any>>;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
18
39
|
entities: {};
|
|
19
40
|
integrations: {};
|
|
20
41
|
auth: {
|
|
@@ -33,6 +54,8 @@ export declare function createClient(config: {
|
|
|
33
54
|
functions: {
|
|
34
55
|
invoke(functionName: string, data: Record<string, any>): Promise<import("axios").AxiosResponse<any, any>>;
|
|
35
56
|
};
|
|
57
|
+
};
|
|
58
|
+
export declare function createClientFromRequest(request: Request): {
|
|
36
59
|
/**
|
|
37
60
|
* Set authentication token for all requests
|
|
38
61
|
* @param {string} newToken - New auth token
|
|
@@ -45,7 +68,31 @@ export declare function createClient(config: {
|
|
|
45
68
|
getConfig(): {
|
|
46
69
|
serverUrl: string;
|
|
47
70
|
appId: string;
|
|
48
|
-
env: string;
|
|
49
71
|
requiresAuth: boolean;
|
|
50
72
|
};
|
|
73
|
+
asServiceRole: {
|
|
74
|
+
entities: {};
|
|
75
|
+
integrations: {};
|
|
76
|
+
functions: {
|
|
77
|
+
invoke(functionName: string, data: Record<string, any>): Promise<import("axios").AxiosResponse<any, any>>;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
entities: {};
|
|
81
|
+
integrations: {};
|
|
82
|
+
auth: {
|
|
83
|
+
me(): Promise<import("axios").AxiosResponse<any, any>>;
|
|
84
|
+
getSsoAccessToken(): Promise<import("axios").AxiosResponse<any, any>>;
|
|
85
|
+
updateMe(data: Record<string, any>): Promise<import("axios").AxiosResponse<any, any>>;
|
|
86
|
+
redirectToLogin(nextUrl: string): void;
|
|
87
|
+
logout(redirectUrl?: string): void;
|
|
88
|
+
setToken(token: string, saveToStorage?: boolean): void;
|
|
89
|
+
loginViaUsernamePassword(email: string, password: string, turnstileToken?: string): Promise<{
|
|
90
|
+
access_token: string;
|
|
91
|
+
user: any;
|
|
92
|
+
}>;
|
|
93
|
+
isAuthenticated(): Promise<boolean>;
|
|
94
|
+
};
|
|
95
|
+
functions: {
|
|
96
|
+
invoke(functionName: string, data: Record<string, any>): Promise<import("axios").AxiosResponse<any, any>>;
|
|
97
|
+
};
|
|
51
98
|
};
|
package/dist/client.js
CHANGED
|
@@ -9,30 +9,27 @@ import { createFunctionsModule } from "./modules/functions.js";
|
|
|
9
9
|
* @param {Object} config - Client configuration
|
|
10
10
|
* @param {string} [config.serverUrl='https://base44.app'] - API server URL
|
|
11
11
|
* @param {string|number} config.appId - Application ID
|
|
12
|
-
* @param {string} [config.env='prod'] - Environment ('prod' or 'dev')
|
|
13
12
|
* @param {string} [config.token] - Authentication token
|
|
13
|
+
* @param {string} [config.serviceToken] - Service role authentication token
|
|
14
14
|
* @param {boolean} [config.requiresAuth=false] - Whether the app requires authentication
|
|
15
15
|
* @returns {Object} Base44 client instance
|
|
16
16
|
*/
|
|
17
17
|
export function createClient(config) {
|
|
18
|
-
const { serverUrl = "https://base44.app", appId,
|
|
19
|
-
// Create the base axios client
|
|
18
|
+
const { serverUrl = "https://base44.app", appId, token, serviceToken, requiresAuth = false, } = config;
|
|
20
19
|
const axiosClient = createAxiosClient({
|
|
21
20
|
baseURL: `${serverUrl}/api`,
|
|
22
21
|
headers: {
|
|
23
22
|
"X-App-Id": String(appId),
|
|
24
|
-
"X-Environment": env,
|
|
25
23
|
},
|
|
26
24
|
token,
|
|
27
|
-
requiresAuth,
|
|
28
|
-
appId,
|
|
29
|
-
serverUrl,
|
|
25
|
+
requiresAuth,
|
|
26
|
+
appId,
|
|
27
|
+
serverUrl,
|
|
30
28
|
});
|
|
31
29
|
const functionsAxiosClient = createAxiosClient({
|
|
32
30
|
baseURL: `${serverUrl}/api`,
|
|
33
31
|
headers: {
|
|
34
32
|
"X-App-Id": String(appId),
|
|
35
|
-
"X-Environment": env,
|
|
36
33
|
},
|
|
37
34
|
token,
|
|
38
35
|
requiresAuth,
|
|
@@ -40,17 +37,42 @@ export function createClient(config) {
|
|
|
40
37
|
serverUrl,
|
|
41
38
|
interceptResponses: false,
|
|
42
39
|
});
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
const serviceRoleAxiosClient = createAxiosClient({
|
|
41
|
+
baseURL: `${serverUrl}/api`,
|
|
42
|
+
headers: {
|
|
43
|
+
"X-App-Id": String(appId),
|
|
44
|
+
},
|
|
45
|
+
token: serviceToken,
|
|
46
|
+
serverUrl,
|
|
47
|
+
appId,
|
|
48
|
+
});
|
|
49
|
+
const serviceRoleFunctionsAxiosClient = createAxiosClient({
|
|
50
|
+
baseURL: `${serverUrl}/api`,
|
|
51
|
+
headers: {
|
|
52
|
+
"X-App-Id": String(appId),
|
|
53
|
+
},
|
|
54
|
+
token: serviceToken,
|
|
55
|
+
serverUrl,
|
|
56
|
+
appId,
|
|
57
|
+
interceptResponses: false,
|
|
58
|
+
});
|
|
59
|
+
const userModules = {
|
|
60
|
+
entities: createEntitiesModule(axiosClient, appId),
|
|
61
|
+
integrations: createIntegrationsModule(axiosClient, appId),
|
|
62
|
+
auth: createAuthModule(axiosClient, functionsAxiosClient, appId),
|
|
63
|
+
functions: createFunctionsModule(functionsAxiosClient, appId),
|
|
64
|
+
};
|
|
65
|
+
const serviceRoleModules = {
|
|
66
|
+
entities: createEntitiesModule(serviceRoleAxiosClient, appId),
|
|
67
|
+
integrations: createIntegrationsModule(serviceRoleAxiosClient, appId),
|
|
68
|
+
functions: createFunctionsModule(serviceRoleFunctionsAxiosClient, appId),
|
|
69
|
+
};
|
|
48
70
|
// Always try to get token from localStorage or URL parameters
|
|
49
71
|
if (typeof window !== "undefined") {
|
|
50
72
|
// Get token from URL or localStorage
|
|
51
73
|
const accessToken = token || getAccessToken();
|
|
52
74
|
if (accessToken) {
|
|
53
|
-
auth.setToken(accessToken);
|
|
75
|
+
userModules.auth.setToken(accessToken);
|
|
54
76
|
}
|
|
55
77
|
}
|
|
56
78
|
// If authentication is required, verify token and redirect to login if needed
|
|
@@ -58,29 +80,26 @@ export function createClient(config) {
|
|
|
58
80
|
// We perform this check asynchronously to not block client creation
|
|
59
81
|
setTimeout(async () => {
|
|
60
82
|
try {
|
|
61
|
-
const isAuthenticated = await auth.isAuthenticated();
|
|
83
|
+
const isAuthenticated = await userModules.auth.isAuthenticated();
|
|
62
84
|
if (!isAuthenticated) {
|
|
63
|
-
auth.redirectToLogin(window.location.href);
|
|
85
|
+
userModules.auth.redirectToLogin(window.location.href);
|
|
64
86
|
}
|
|
65
87
|
}
|
|
66
88
|
catch (error) {
|
|
67
89
|
console.error("Authentication check failed:", error);
|
|
68
|
-
auth.redirectToLogin(window.location.href);
|
|
90
|
+
userModules.auth.redirectToLogin(window.location.href);
|
|
69
91
|
}
|
|
70
92
|
}, 0);
|
|
71
93
|
}
|
|
72
94
|
// Assemble and return the client
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
integrations,
|
|
76
|
-
auth,
|
|
77
|
-
functions,
|
|
95
|
+
const client = {
|
|
96
|
+
...userModules,
|
|
78
97
|
/**
|
|
79
98
|
* Set authentication token for all requests
|
|
80
99
|
* @param {string} newToken - New auth token
|
|
81
100
|
*/
|
|
82
101
|
setToken(newToken) {
|
|
83
|
-
auth.setToken(newToken);
|
|
102
|
+
userModules.auth.setToken(newToken);
|
|
84
103
|
},
|
|
85
104
|
/**
|
|
86
105
|
* Get current configuration
|
|
@@ -90,9 +109,49 @@ export function createClient(config) {
|
|
|
90
109
|
return {
|
|
91
110
|
serverUrl,
|
|
92
111
|
appId,
|
|
93
|
-
env,
|
|
94
112
|
requiresAuth,
|
|
95
113
|
};
|
|
96
114
|
},
|
|
115
|
+
/**
|
|
116
|
+
* Access service role modules - throws error if no service token was provided
|
|
117
|
+
* @throws {Error} When accessed without a service token
|
|
118
|
+
*/
|
|
119
|
+
get asServiceRole() {
|
|
120
|
+
if (!serviceToken) {
|
|
121
|
+
throw new Error('Service token is required to use asServiceRole. Please provide a serviceToken when creating the client.');
|
|
122
|
+
}
|
|
123
|
+
return serviceRoleModules;
|
|
124
|
+
}
|
|
97
125
|
};
|
|
126
|
+
return client;
|
|
127
|
+
}
|
|
128
|
+
export function createClientFromRequest(request) {
|
|
129
|
+
const authHeader = request.headers.get("Authorization");
|
|
130
|
+
const serviceRoleAuthHeader = request.headers.get("Base44-Service-Authorization");
|
|
131
|
+
const appId = request.headers.get("Base44-App-Id");
|
|
132
|
+
const serverUrlHeader = request.headers.get("Base44-Api-Url");
|
|
133
|
+
if (!appId) {
|
|
134
|
+
throw new Error("Base44-App-Id header is required, but is was not found on the request");
|
|
135
|
+
}
|
|
136
|
+
// Validate authorization header formats
|
|
137
|
+
let serviceRoleToken;
|
|
138
|
+
let userToken;
|
|
139
|
+
if (serviceRoleAuthHeader !== null) {
|
|
140
|
+
if (serviceRoleAuthHeader === '' || !serviceRoleAuthHeader.startsWith('Bearer ') || serviceRoleAuthHeader.split(' ').length !== 2) {
|
|
141
|
+
throw new Error('Invalid authorization header format. Expected "Bearer <token>"');
|
|
142
|
+
}
|
|
143
|
+
serviceRoleToken = serviceRoleAuthHeader.split(' ')[1];
|
|
144
|
+
}
|
|
145
|
+
if (authHeader !== null) {
|
|
146
|
+
if (authHeader === '' || !authHeader.startsWith('Bearer ') || authHeader.split(' ').length !== 2) {
|
|
147
|
+
throw new Error('Invalid authorization header format. Expected "Bearer <token>"');
|
|
148
|
+
}
|
|
149
|
+
userToken = authHeader.split(' ')[1];
|
|
150
|
+
}
|
|
151
|
+
return createClient({
|
|
152
|
+
serverUrl: serverUrlHeader || "https://base44.app",
|
|
153
|
+
appId,
|
|
154
|
+
token: userToken,
|
|
155
|
+
serviceToken: serviceRoleToken,
|
|
156
|
+
});
|
|
98
157
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createClient } from "./client.js";
|
|
1
|
+
import { createClient, createClientFromRequest } from "./client.js";
|
|
2
2
|
import { Base44Error } from "./utils/axios-client.js";
|
|
3
3
|
import { getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl } from "./utils/auth-utils.js";
|
|
4
|
-
export { createClient, Base44Error, getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, };
|
|
4
|
+
export { createClient, createClientFromRequest, Base44Error, getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { createClient } from "./client.js";
|
|
1
|
+
import { createClient, createClientFromRequest } from "./client.js";
|
|
2
2
|
import { Base44Error } from "./utils/axios-client.js";
|
|
3
3
|
import { getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, } from "./utils/auth-utils.js";
|
|
4
|
-
export { createClient, Base44Error,
|
|
5
|
-
// Export auth utilities for easier access
|
|
6
|
-
getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, };
|
|
4
|
+
export { createClient, createClientFromRequest, Base44Error, getAccessToken, saveAccessToken, removeAccessToken, getLoginUrl, };
|
package/dist/modules/auth.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { AxiosInstance } from "axios";
|
|
|
6
6
|
* @param {string} serverUrl - Server URL
|
|
7
7
|
* @returns {Object} Auth module with authentication methods
|
|
8
8
|
*/
|
|
9
|
-
export declare function createAuthModule(axios: AxiosInstance,
|
|
9
|
+
export declare function createAuthModule(axios: AxiosInstance, functionsAxiosClient: AxiosInstance, appId: string): {
|
|
10
10
|
/**
|
|
11
11
|
* Get current user information
|
|
12
12
|
* @returns {Promise<Object>} Current user data
|
package/dist/modules/auth.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @param {string} serverUrl - Server URL
|
|
6
6
|
* @returns {Object} Auth module with authentication methods
|
|
7
7
|
*/
|
|
8
|
-
export function createAuthModule(axios,
|
|
8
|
+
export function createAuthModule(axios, functionsAxiosClient, appId) {
|
|
9
9
|
return {
|
|
10
10
|
/**
|
|
11
11
|
* Get current user information
|
|
@@ -83,6 +83,7 @@ export function createAuthModule(axios, appId, serverUrl) {
|
|
|
83
83
|
if (!token)
|
|
84
84
|
return;
|
|
85
85
|
axios.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
86
|
+
functionsAxiosClient.defaults.headers.common["Authorization"] = `Bearer ${token}`;
|
|
86
87
|
// Save token to localStorage if requested
|
|
87
88
|
if (saveToStorage &&
|
|
88
89
|
typeof window !== "undefined" &&
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@base44-preview/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-dev.485c22b",
|
|
4
|
+
"description": "JavaScript SDK for Base44 API",
|
|
4
5
|
"main": "dist/index.js",
|
|
5
6
|
"types": "dist/index.d.ts",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"files": [
|
|
8
|
-
"dist"
|
|
9
|
-
"!README.md"
|
|
9
|
+
"dist"
|
|
10
10
|
],
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc",
|
|
@@ -22,18 +22,29 @@
|
|
|
22
22
|
"axios": "^1.6.2"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"vitest": "^1.0.0",
|
|
26
|
-
"@vitest/ui": "^1.0.0",
|
|
27
25
|
"@vitest/coverage-istanbul": "^1.0.0",
|
|
28
26
|
"@vitest/coverage-v8": "^1.0.0",
|
|
27
|
+
"@vitest/ui": "^1.0.0",
|
|
29
28
|
"dotenv": "^16.3.1",
|
|
30
29
|
"eslint": "^8.54.0",
|
|
31
30
|
"nock": "^13.4.0",
|
|
32
|
-
"typescript": "^5.3.2"
|
|
31
|
+
"typescript": "^5.3.2",
|
|
32
|
+
"vitest": "^1.0.0"
|
|
33
33
|
},
|
|
34
34
|
"keywords": [
|
|
35
|
+
"base44",
|
|
35
36
|
"api",
|
|
36
37
|
"sdk"
|
|
37
38
|
],
|
|
38
|
-
"
|
|
39
|
+
"author": "Base44",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/base44/sdk.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/base44/sdk/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/base44/sdk#readme",
|
|
49
|
+
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
|
39
50
|
}
|