@ticatec/common-express-server 0.0.7 → 0.1.2

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.
Files changed (42) hide show
  1. package/README.md +491 -0
  2. package/lib/AppConf.d.ts +31 -0
  3. package/lib/AppConf.js +57 -0
  4. package/lib/AppConf.js.map +1 -0
  5. package/lib/BaseServer.d.ts +72 -26
  6. package/lib/BaseServer.js +52 -24
  7. package/lib/BaseServer.js.map +1 -1
  8. package/lib/CommonRouterHelper.d.ts +45 -22
  9. package/lib/CommonRouterHelper.js +52 -22
  10. package/lib/CommonRouterHelper.js.map +1 -1
  11. package/lib/CommonRoutes.d.ts +23 -5
  12. package/lib/CommonRoutes.js +17 -2
  13. package/lib/CommonRoutes.js.map +1 -1
  14. package/lib/LoggedUser.d.ts +11 -15
  15. package/lib/common/AdminBaseController.d.ts +14 -10
  16. package/lib/common/AdminBaseController.js +11 -8
  17. package/lib/common/AdminBaseController.js.map +1 -1
  18. package/lib/common/AdminSearchController.d.ts +6 -1
  19. package/lib/common/AdminSearchController.js +7 -2
  20. package/lib/common/AdminSearchController.js.map +1 -1
  21. package/lib/common/BaseController.d.ts +18 -2
  22. package/lib/common/BaseController.js +18 -3
  23. package/lib/common/BaseController.js.map +1 -1
  24. package/lib/common/CommonController.d.ts +49 -25
  25. package/lib/common/CommonController.js +42 -23
  26. package/lib/common/CommonController.js.map +1 -1
  27. package/lib/common/TenantBaseController.d.ts +16 -11
  28. package/lib/common/TenantBaseController.js +14 -13
  29. package/lib/common/TenantBaseController.js.map +1 -1
  30. package/lib/common/TenantSearchController.d.ts +6 -1
  31. package/lib/common/TenantSearchController.js +7 -2
  32. package/lib/common/TenantSearchController.js.map +1 -1
  33. package/lib/index.d.ts +14 -0
  34. package/lib/index.js +30 -0
  35. package/lib/index.js.map +1 -0
  36. package/package.json +54 -14
  37. package/.idea/common-express-server.iml +0 -9
  38. package/.idea/inspectionProfiles/Project_Default.xml +0 -11
  39. package/.idea/misc.xml +0 -5
  40. package/.idea/modules.xml +0 -8
  41. package/.idea/vcs.xml +0 -6
  42. package/tsconfig.json +0 -13
package/README.md ADDED
@@ -0,0 +1,491 @@
1
+ # @ticatec/common-express-server
2
+
3
+ [![npm version](https://badge.fury.io/js/@ticatec%2Fcommon-express-server.svg)](https://badge.fury.io/js/@ticatec%2Fcommon-express-server)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ A comprehensive TypeScript library providing common classes, controllers, and middleware for building scalable Express.js applications with multi-tenant support.
7
+
8
+ [中文](./README_CN.md) | English
9
+
10
+ ## Features
11
+
12
+ - 🚀 **Express.js Foundation**: Built on Express.js 5.x with full TypeScript support
13
+ - 🏢 **Multi-tenant Architecture**: Built-in support for multi-tenant applications
14
+ - 🔐 **Authentication & Authorization**: User authentication and role-based access control
15
+ - 🎯 **Controller Patterns**: Pre-built base controllers for common CRUD operations
16
+ - 📝 **Validation**: Integrated data validation using bean-validator
17
+ - 🔄 **Error Handling**: Centralized error handling and logging
18
+ - 🌐 **Internationalization**: Built-in language support via headers
19
+ - 📊 **Logging**: Structured logging with log4js integration
20
+ - 🎨 **TypeScript First**: Full TypeScript support with comprehensive type definitions
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install @ticatec/common-express-server
26
+ ```
27
+
28
+ ### Peer Dependencies
29
+
30
+ ```bash
31
+ npm install express@^5.1.0
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ### 1. Create a Basic Server
37
+
38
+ ```typescript
39
+ import { BaseServer, CommonRouterHelper } from '@ticatec/common-express-server';
40
+
41
+ class MyRouterHelper extends CommonRouterHelper {
42
+ // Add custom middleware or override methods as needed
43
+ }
44
+
45
+ class MyServer extends BaseServer<MyRouterHelper> {
46
+ protected getHelper(): MyRouterHelper {
47
+ return new MyRouterHelper();
48
+ }
49
+
50
+ protected async loadConfigFile(): Promise<void> {
51
+ // Load your configuration here
52
+ console.log('Loading configuration...');
53
+ }
54
+
55
+ protected getWebConf() {
56
+ return {
57
+ port: 3000,
58
+ ip: '0.0.0.0',
59
+ contextRoot: '/api'
60
+ };
61
+ }
62
+
63
+ protected async setupRoutes(app: Express): Promise<void> {
64
+ // Set up your routes here
65
+ await this.bindRoutes(app, '/users', () => import('./routes/UserRoutes'));
66
+ }
67
+ }
68
+
69
+ // Start the server
70
+ const server = new MyServer();
71
+ BaseServer.startup(server);
72
+ ```
73
+
74
+ ### 2. Create Routes
75
+
76
+ ```typescript
77
+ import { CommonRoutes, CommonRouterHelper } from '@ticatec/common-express-server';
78
+
79
+ class UserRoutes extends CommonRoutes<CommonRouterHelper> {
80
+ constructor(helper: CommonRouterHelper) {
81
+ super(helper); // checkUser = true by default
82
+ this.setupRoutes();
83
+ }
84
+
85
+ private setupRoutes() {
86
+ this.router.get('/profile', this.helper.invokeRestfulAction(this.getProfile));
87
+ this.router.post('/update', this.helper.invokeRestfulAction(this.updateProfile));
88
+ }
89
+
90
+ private getProfile = async (req: Request) => {
91
+ // Your logic here
92
+ return { message: 'User profile' };
93
+ };
94
+
95
+ private updateProfile = async (req: Request) => {
96
+ // Your logic here
97
+ return { message: 'Profile updated' };
98
+ };
99
+ }
100
+
101
+ export default UserRoutes;
102
+ ```
103
+
104
+ ### 3. Create Controllers
105
+
106
+ ```typescript
107
+ import { TenantBaseController } from '@ticatec/common-express-server';
108
+ import { ValidationRules, StringValidator } from '@ticatec/bean-validator';
109
+
110
+ interface UserService {
111
+ createNew(user: any, data: any): Promise<any>;
112
+ update(user: any, data: any): Promise<any>;
113
+ search(user: any, query: any): Promise<any>;
114
+ }
115
+
116
+ const userValidationRules: ValidationRules = [
117
+ new StringValidator('name', { required: true, minLen: 2 }),
118
+ new StringValidator('email', {
119
+ required: true,
120
+ format: {
121
+ regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
122
+ message: 'Invalid email format'
123
+ }
124
+ })
125
+ ];
126
+
127
+ class UserController extends TenantBaseController<UserService> {
128
+ constructor(userService: UserService) {
129
+ super(userService, userValidationRules);
130
+ }
131
+
132
+ // CRUD methods are inherited and automatically validated
133
+ // createNew(), update(), del() are available
134
+
135
+ // Add custom methods
136
+ search() {
137
+ return async (req: Request): Promise<any> => {
138
+ const query = req.query;
139
+ this.checkInterface('search');
140
+ return await this.invokeServiceInterface('search', [
141
+ this.getLoggedUser(req),
142
+ query
143
+ ]);
144
+ };
145
+ }
146
+ }
147
+ ```
148
+
149
+ ## Core Classes
150
+
151
+ ### BaseServer<T>
152
+
153
+ Abstract base server class that provides:
154
+ - Express application setup
155
+ - Configuration loading
156
+ - Route binding
157
+ - Error handling
158
+ - Health check endpoint
159
+ - Static file serving
160
+
161
+ ### CommonRouterHelper
162
+
163
+ Middleware utilities for:
164
+ - JSON response formatting
165
+ - Cache control
166
+ - User authentication
167
+ - Error handling
168
+ - Request logging
169
+
170
+ ### CommonRoutes<T>
171
+
172
+ Base class for route definitions with:
173
+ - Express router integration
174
+ - User authentication checks
175
+ - Logging capabilities
176
+
177
+ ### Controllers Hierarchy
178
+
179
+ - **BaseController<T>**: Basic controller with logging and user context
180
+ - **CommonController<T>**: CRUD operations with validation
181
+ - **AdminBaseController<T>**: Admin-specific operations (tenant-independent)
182
+ - **TenantBaseController<T>**: Tenant-specific operations
183
+ - **AdminSearchController<T>**: Admin search operations
184
+ - **TenantSearchController<T>**: Tenant search operations
185
+
186
+ ## Configuration
187
+
188
+ ### Application Configuration
189
+
190
+ ```typescript
191
+ import { AppConf } from '@ticatec/common-express-server';
192
+
193
+ // Initialize configuration
194
+ AppConf.init({
195
+ database: {
196
+ host: 'localhost',
197
+ port: 5432
198
+ },
199
+ server: {
200
+ port: 3000
201
+ }
202
+ });
203
+
204
+ // Use configuration
205
+ const config = AppConf.getInstance();
206
+ const dbHost = config.get('database.host');
207
+ const serverPort = config.get('server.port');
208
+ ```
209
+
210
+ ### Gateway Architecture
211
+
212
+ This application is designed to work behind an API Gateway. The gateway handles JWT tokens or session-based authentication and forwards the authenticated user information to the Express application via HTTP headers.
213
+
214
+ #### Architecture Flow
215
+
216
+ ```
217
+ Client Request (JWT/Session) → API Gateway → Express Application
218
+
219
+ User Info Headers
220
+ ```
221
+
222
+ #### Gateway Responsibilities
223
+
224
+ The API Gateway should:
225
+
226
+ 1. **Authenticate requests** using JWT tokens, session cookies, or other authentication mechanisms
227
+ 2. **Extract user information** from the authentication token/session
228
+ 3. **Forward user data** as HTTP headers to the Express application
229
+ 4. **Handle authorization** and rate limiting as needed
230
+
231
+ #### User Authentication Headers
232
+
233
+ The library expects user information in the request headers:
234
+
235
+ ```typescript
236
+ // Headers forwarded by the gateway
237
+ {
238
+ 'user': encodeURIComponent(JSON.stringify({
239
+ accountCode: 'user123',
240
+ name: 'John Doe',
241
+ tenant: { code: 'tenant1', name: 'Tenant One' }
242
+ })),
243
+ 'x-language': 'en'
244
+ }
245
+ ```
246
+
247
+ #### Gateway Implementation Example
248
+
249
+ Here's an example of how the gateway might process authentication:
250
+
251
+ ```typescript
252
+ // Gateway middleware (pseudo-code)
253
+ async function processAuthentication(request) {
254
+ // 1. Validate JWT token or session
255
+ const token = request.headers.authorization?.replace('Bearer ', '');
256
+ const userInfo = await validateJWT(token);
257
+
258
+ // 2. Extract user information
259
+ const user = {
260
+ accountCode: userInfo.sub,
261
+ name: userInfo.name,
262
+ tenant: {
263
+ code: userInfo.tenant_code,
264
+ name: userInfo.tenant_name
265
+ }
266
+ };
267
+
268
+ // 3. Forward to Express app with user header
269
+ request.headers['user'] = encodeURIComponent(JSON.stringify(user));
270
+ request.headers['x-language'] = userInfo.preferred_language || 'en';
271
+
272
+ // 4. Proxy request to Express application
273
+ return proxyToExpressApp(request);
274
+ }
275
+ ```
276
+
277
+ #### Security Considerations
278
+
279
+ - **No direct authentication**: This Express application does NOT handle JWT validation or session management
280
+ - **Trust boundary**: The application trusts that the gateway has properly authenticated users
281
+ - **Header validation**: User headers are parsed and validated but not authenticated
282
+ - **Network security**: Ensure secure communication between gateway and Express app (internal network/VPN)
283
+
284
+ ### User Impersonation
285
+
286
+ The library supports user impersonation, which allows privileged system users to act as another user (including cross-tenant operations) for debugging and troubleshooting purposes.
287
+
288
+ #### How It Works
289
+
290
+ When a privileged user needs to impersonate another user, the gateway should include both the original user and the target user in the headers:
291
+
292
+ ```typescript
293
+ // Headers with user impersonation
294
+ {
295
+ 'user': encodeURIComponent(JSON.stringify({
296
+ // Original privileged user
297
+ accountCode: 'admin123',
298
+ name: 'System Admin',
299
+ tenant: { code: 'system', name: 'System Tenant' },
300
+
301
+ // User being impersonated
302
+ actAs: {
303
+ accountCode: 'user456',
304
+ name: 'Target User',
305
+ tenant: { code: 'client-a', name: 'Client A' }
306
+ }
307
+ })),
308
+ 'x-language': 'en'
309
+ }
310
+ ```
311
+
312
+ #### Implementation in Controllers
313
+
314
+ The `BaseController.getLoggedUser()` method automatically handles impersonation:
315
+
316
+ ```typescript
317
+ class MyController extends TenantBaseController<MyService> {
318
+
319
+ async getUserData(req: Request) {
320
+ // This will return the impersonated user if actAs is present,
321
+ // otherwise returns the original user
322
+ const currentUser = this.getLoggedUser(req);
323
+
324
+ console.log('Operating as:', currentUser.name);
325
+ console.log('Tenant context:', currentUser.tenant.code);
326
+
327
+ // All operations will be performed in the context of the impersonated user
328
+ return await this.service.getUserData(currentUser);
329
+ }
330
+ }
331
+ ```
332
+
333
+
334
+ #### Use Cases
335
+
336
+ - **Debug user issues**: Support staff can reproduce problems by acting as the affected user
337
+ - **Cross-tenant troubleshooting**: System administrators can debug issues across different tenants
338
+ - **Testing user permissions**: Verify that user-specific access controls work correctly
339
+ - **Data migration**: Perform operations on behalf of users during system migrations
340
+
341
+ #### Express Application Responsibilities
342
+
343
+ The Express application simply:
344
+ - **Trusts the gateway**: Accepts user impersonation information from authenticated gateway requests
345
+ - **Processes context**: Uses the `actAs` user for all business operations when present
346
+ - **No validation**: Does not validate impersonation privileges or restrictions
347
+
348
+ #### Gateway Responsibilities for Impersonation
349
+
350
+ All impersonation controls should be handled by the gateway:
351
+ - **Privilege validation**: Verify users have permission to impersonate others
352
+ - **Audit logging**: Record all impersonation activities for security auditing
353
+ - **Time limits**: Implement time-based restrictions on impersonation sessions
354
+ - **Session management**: Handle impersonation session lifecycle
355
+ - **Cross-tenant controls**: Apply additional checks for cross-tenant impersonation
356
+ - **Notification**: Optionally notify target users when their accounts are being impersonated
357
+
358
+ ## Multi-tenant Support
359
+
360
+ The library provides built-in multi-tenant support:
361
+
362
+ ```typescript
363
+ // Tenant-specific controller
364
+ class ProductController extends TenantBaseController<ProductService> {
365
+ // Automatically receives logged user context
366
+ // All operations are tenant-scoped
367
+ }
368
+
369
+ // Admin controller (cross-tenant)
370
+ class SystemController extends AdminBaseController<SystemService> {
371
+ // Operations across all tenants
372
+ }
373
+ ```
374
+
375
+ ## Validation
376
+
377
+ Built-in validation using `@ticatec/bean-validator`:
378
+
379
+ ```typescript
380
+ import { ValidationRules, StringValidator, NumberValidator } from '@ticatec/bean-validator';
381
+
382
+ const rules: ValidationRules = [
383
+ new StringValidator('name', { required: true, minLen: 2, maxLen: 50 }),
384
+ new StringValidator('email', {
385
+ required: true,
386
+ format: {
387
+ regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
388
+ message: 'Invalid email format'
389
+ }
390
+ }),
391
+ new NumberValidator('age', { required: false, minValue: 18, maxValue: 120 })
392
+ ];
393
+
394
+ class UserController extends CommonController<UserService> {
395
+ constructor(service: UserService) {
396
+ super(service, rules); // Validation applied automatically
397
+ }
398
+ }
399
+ ```
400
+
401
+ ## Error Handling
402
+
403
+ Centralized error handling with `@ticatec/express-exception`:
404
+
405
+ ```typescript
406
+ import {
407
+ ActionNotFoundError,
408
+ UnauthenticatedError,
409
+ IllegalParameterError
410
+ } from '@ticatec/express-exception';
411
+
412
+ // Errors are automatically handled and formatted
413
+ throw new ActionNotFoundError('Resource not found');
414
+ throw new UnauthenticatedError('User not authenticated');
415
+ throw new IllegalParameterError('Invalid input data');
416
+ ```
417
+
418
+ ## API Reference
419
+
420
+ ### Types
421
+
422
+ ```typescript
423
+ // Function signatures
424
+ export type RestfulFunction = (req: Request) => any;
425
+ export type ControlFunction = (req: Request, res: Response) => any;
426
+ export type moduleLoader = () => Promise<any>;
427
+
428
+ // Validation types (from @ticatec/bean-validator)
429
+ export type ValidationRules = Array<BaseValidator>;
430
+
431
+ // User interfaces
432
+ export interface CommonUser {
433
+ accountCode: string;
434
+ name: string;
435
+ tenant: {
436
+ code: string;
437
+ name: string;
438
+ };
439
+ [key: string]: any;
440
+ }
441
+
442
+ export interface LoggedUser extends CommonUser {
443
+ actAs?: CommonUser; // For user impersonation
444
+ }
445
+ ```
446
+
447
+ ## Development
448
+
449
+ ### Build
450
+
451
+ ```bash
452
+ npm run build # Build the project
453
+ npm run dev # Development mode with watch
454
+ ```
455
+
456
+ ## Requirements
457
+
458
+ - Node.js >= 18.0.0
459
+ - Express.js ^5.1.0
460
+ - TypeScript ^5.0.0
461
+
462
+ ## Dependencies
463
+
464
+ - `@ticatec/bean-validator`: Data validation
465
+ - `@ticatec/express-exception`: Error handling
466
+ - `@ticatec/node-common-library`: Common utilities
467
+ - `log4js`: Logging framework
468
+
469
+ ## Contributing
470
+
471
+ 1. Fork the repository
472
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
473
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
474
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
475
+ 5. Open a Pull Request
476
+
477
+ ## License
478
+
479
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
480
+
481
+ ## Support
482
+
483
+ For support and questions:
484
+
485
+ - 📧 Email: henry@ticatec.com
486
+ - 🐛 Issues: [GitHub Issues](https://github.com/ticatec/common-express-server/issues)
487
+ - 📚 Documentation: [GitHub Repository](https://github.com/ticatec/common-express-server)
488
+
489
+ ---
490
+
491
+ Made with ❤️ by [TicaTec](https://github.com/ticatec)
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Application configuration singleton class
3
+ */
4
+ export default class AppConf {
5
+ /** Configuration object */
6
+ private readonly conf;
7
+ /** Singleton instance */
8
+ static instance: AppConf;
9
+ /**
10
+ * Private constructor for singleton pattern
11
+ * @param conf Configuration object
12
+ */
13
+ private constructor();
14
+ /**
15
+ * Gets the singleton instance
16
+ * @returns AppConf instance or null if not initialized
17
+ */
18
+ static getInstance(): AppConf;
19
+ /**
20
+ * Initializes the configuration singleton
21
+ * @param config Configuration object
22
+ * @returns AppConf instance
23
+ */
24
+ static init(config: any): AppConf;
25
+ /**
26
+ * Gets configuration value by key (supports dot notation)
27
+ * @param key Configuration key (can use dot notation like 'server.port')
28
+ * @returns Configuration value or undefined if not found
29
+ */
30
+ get(key: string): any;
31
+ }
package/lib/AppConf.js ADDED
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Application configuration singleton class
5
+ */
6
+ class AppConf {
7
+ /**
8
+ * Private constructor for singleton pattern
9
+ * @param conf Configuration object
10
+ */
11
+ constructor(conf) {
12
+ this.conf = conf;
13
+ }
14
+ /**
15
+ * Gets the singleton instance
16
+ * @returns AppConf instance or null if not initialized
17
+ */
18
+ static getInstance() {
19
+ return AppConf.instance;
20
+ }
21
+ /**
22
+ * Initializes the configuration singleton
23
+ * @param config Configuration object
24
+ * @returns AppConf instance
25
+ */
26
+ static init(config) {
27
+ console.debug('Initializing configuration center', config);
28
+ if (AppConf.instance == null) {
29
+ AppConf.instance = new AppConf(config);
30
+ }
31
+ return AppConf.instance;
32
+ }
33
+ /**
34
+ * Gets configuration value by key (supports dot notation)
35
+ * @param key Configuration key (can use dot notation like 'server.port')
36
+ * @returns Configuration value or undefined if not found
37
+ */
38
+ get(key) {
39
+ if (!key)
40
+ return undefined;
41
+ const keys = key.split('.');
42
+ let result = this.conf;
43
+ for (const k of keys) {
44
+ if (result && typeof result === 'object' && k in result) {
45
+ result = result[k];
46
+ }
47
+ else {
48
+ return undefined;
49
+ }
50
+ }
51
+ return result;
52
+ }
53
+ }
54
+ /** Singleton instance */
55
+ AppConf.instance = null;
56
+ exports.default = AppConf;
57
+ //# sourceMappingURL=AppConf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AppConf.js","sourceRoot":"src/","sources":["AppConf.ts"],"names":[],"mappings":";;AAAA;;GAEG;AACH,MAAqB,OAAO;IAOxB;;;OAGG;IACH,YAAoB,IAAS;QACzB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACrB,CAAC;IAED;;;OAGG;IACH,MAAM,CAAC,WAAW;QACd,OAAO,OAAO,CAAC,QAAQ,CAAC;IAC5B,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,IAAI,CAAC,MAAW;QACnB,OAAO,CAAC,KAAK,CAAC,mCAAmC,EAAE,MAAM,CAAC,CAAC;QAC3D,IAAI,OAAO,CAAC,QAAQ,IAAI,IAAI,EAAE,CAAC;YAC3B,OAAO,CAAC,QAAQ,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;QAC3C,CAAC;QACD,OAAO,OAAO,CAAC,QAAQ,CAAC;IAC5B,CAAC;IAED;;;;OAIG;IACH,GAAG,CAAC,GAAW;QACX,IAAI,CAAC,GAAG;YAAE,OAAO,SAAS,CAAC;QAE3B,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC;QAEvB,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACnB,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,IAAI,MAAM,EAAE,CAAC;gBACtD,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACvB,CAAC;iBAAM,CAAC;gBACJ,OAAO,SAAS,CAAC;YACrB,CAAC;QACL,CAAC;QAED,OAAO,MAAM,CAAC;IAClB,CAAC;;AApDD,yBAAyB;AAClB,gBAAQ,GAAY,IAAI,CAAC;kBALf,OAAO"}