@explorins/pers-sdk 1.0.0-alpha.1
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/package.json +52 -0
- package/rollup.config.js +78 -0
- package/src/business/api/business-api.ts +72 -0
- package/src/business/business/tsconfig.json +18 -0
- package/src/business/index.ts +52 -0
- package/src/business/models/index.ts +13 -0
- package/src/business/services/business-service.ts +88 -0
- package/src/core/abstractions/core-interfaces.ts +56 -0
- package/src/core/abstractions/http-client.ts +23 -0
- package/src/core/auth/auth-provider.interface.ts +16 -0
- package/src/core/auth/create-auth-provider.ts +136 -0
- package/src/core/auth/simple-auth-config.interface.ts +15 -0
- package/src/core/core.ts +30 -0
- package/src/core/pers-api-client.ts +151 -0
- package/src/core/pers-config.ts +38 -0
- package/src/core.ts +30 -0
- package/src/index.ts +16 -0
- package/src/pers-sdk.ts +45 -0
- package/tsconfig.json +28 -0
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@explorins/pers-sdk",
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
4
|
+
"description": "Platform-agnostic SDK for PERS (Phygital Experience Rewards System)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.esm.js",
|
|
10
|
+
"require": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./core": {
|
|
13
|
+
"types": "./dist/core/core.d.ts",
|
|
14
|
+
"import": "./dist/core.esm.js",
|
|
15
|
+
"require": "./dist/core.js"
|
|
16
|
+
},
|
|
17
|
+
"./business": {
|
|
18
|
+
"types": "./dist/business/index.d.ts",
|
|
19
|
+
"import": "./dist/business.esm.js",
|
|
20
|
+
"require": "./dist/business.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "rollup -c",
|
|
25
|
+
"build:watch": "rollup -c --watch",
|
|
26
|
+
"clean": "rimraf dist",
|
|
27
|
+
"test": "jest",
|
|
28
|
+
"test:watch": "jest --watch",
|
|
29
|
+
"lint": "eslint src/**/*.ts"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@explorins/pers-shared": "*"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"typescript": "^5.4.5",
|
|
36
|
+
"@types/jest": "^29.5.12",
|
|
37
|
+
"jest": "^29.7.0",
|
|
38
|
+
"@rollup/plugin-typescript": "^11.1.6",
|
|
39
|
+
"rollup": "^4.50.0",
|
|
40
|
+
"rimraf": "^5.0.5"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@explorins/pers-shared": "*"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"registry": "https://registry.npmjs.org/"
|
|
48
|
+
},
|
|
49
|
+
"keywords": ["pers", "business", "sdk", "platform-agnostic"],
|
|
50
|
+
"author": "Explorins",
|
|
51
|
+
"license": "MIT"
|
|
52
|
+
}
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import typescript from '@rollup/plugin-typescript';
|
|
2
|
+
|
|
3
|
+
export default [
|
|
4
|
+
// Core entry point
|
|
5
|
+
{
|
|
6
|
+
input: 'src/core/core.ts',
|
|
7
|
+
output: [
|
|
8
|
+
{
|
|
9
|
+
file: 'dist/core.js',
|
|
10
|
+
format: 'cjs',
|
|
11
|
+
sourcemap: true
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
file: 'dist/core.esm.js',
|
|
15
|
+
format: 'esm',
|
|
16
|
+
sourcemap: true
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
plugins: [
|
|
20
|
+
typescript({
|
|
21
|
+
tsconfig: './tsconfig.json',
|
|
22
|
+
declaration: true,
|
|
23
|
+
declarationDir: './dist'
|
|
24
|
+
})
|
|
25
|
+
],
|
|
26
|
+
external: ['rxjs', 'rxjs/operators']
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// Business domain entry point
|
|
30
|
+
{
|
|
31
|
+
input: 'src/business/index.ts',
|
|
32
|
+
output: [
|
|
33
|
+
{
|
|
34
|
+
file: 'dist/business.js',
|
|
35
|
+
format: 'cjs',
|
|
36
|
+
sourcemap: true
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
file: 'dist/business.esm.js',
|
|
40
|
+
format: 'esm',
|
|
41
|
+
sourcemap: true
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
plugins: [
|
|
45
|
+
typescript({
|
|
46
|
+
tsconfig: './tsconfig.json',
|
|
47
|
+
declaration: true,
|
|
48
|
+
declarationDir: './dist'
|
|
49
|
+
})
|
|
50
|
+
],
|
|
51
|
+
external: ['@explorins/pers-shared', 'rxjs', 'rxjs/operators']
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// Main entry point (everything)
|
|
55
|
+
{
|
|
56
|
+
input: 'src/index.ts',
|
|
57
|
+
output: [
|
|
58
|
+
{
|
|
59
|
+
file: 'dist/index.js',
|
|
60
|
+
format: 'cjs',
|
|
61
|
+
sourcemap: true
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
file: 'dist/index.esm.js',
|
|
65
|
+
format: 'esm',
|
|
66
|
+
sourcemap: true
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
plugins: [
|
|
70
|
+
typescript({
|
|
71
|
+
tsconfig: './tsconfig.json',
|
|
72
|
+
declaration: true,
|
|
73
|
+
declarationDir: './dist'
|
|
74
|
+
})
|
|
75
|
+
],
|
|
76
|
+
external: ['@explorins/pers-shared', 'rxjs', 'rxjs/operators']
|
|
77
|
+
}
|
|
78
|
+
];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { PersApiClient } from '../../core/pers-api-client';
|
|
2
|
+
import { BusinessDTO, BusinessTypeDTO, BusinessUpdateRequestDTO } from '@explorins/pers-shared';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Platform-Agnostic Business API Client
|
|
6
|
+
*
|
|
7
|
+
* Focuses on non-admin business operations using the PERS backend.
|
|
8
|
+
* Uses @explorins/pers-shared DTOs for consistency with backend.
|
|
9
|
+
*/
|
|
10
|
+
export class BusinessApi {
|
|
11
|
+
constructor(private apiClient: PersApiClient) {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get all active businesses (public endpoint)
|
|
15
|
+
*/
|
|
16
|
+
async getActiveBusinesses(): Promise<BusinessDTO[]> {
|
|
17
|
+
return this.apiClient.get<BusinessDTO[]>('/business');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get all business types (public endpoint)
|
|
22
|
+
*/
|
|
23
|
+
async getAllBusinessTypes(): Promise<BusinessTypeDTO[]> {
|
|
24
|
+
return this.apiClient.get<BusinessTypeDTO[]>('/business/type');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get business by ID
|
|
29
|
+
*/
|
|
30
|
+
async getBusinessById(businessId: string): Promise<BusinessDTO> {
|
|
31
|
+
return this.apiClient.get<BusinessDTO>(`/business/${businessId}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get business by account address
|
|
36
|
+
*/
|
|
37
|
+
async getBusinessByAccount(accountAddress: string): Promise<BusinessDTO> {
|
|
38
|
+
return this.apiClient.get<BusinessDTO>(`/business/account/${accountAddress}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ==========================================
|
|
42
|
+
// ADMIN OPERATIONS
|
|
43
|
+
// ==========================================
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* ADMIN: Get all businesses (active and inactive)
|
|
47
|
+
*/
|
|
48
|
+
async getAllBusinesses(): Promise<BusinessDTO[]> {
|
|
49
|
+
return this.apiClient.get<BusinessDTO[]>('/business/admin');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* ADMIN: Create business by display name
|
|
54
|
+
*/
|
|
55
|
+
async createBusinessByDisplayName(displayName: string): Promise<BusinessDTO> {
|
|
56
|
+
return this.apiClient.post<BusinessDTO>('/business/admin/', { displayName });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* ADMIN: Update business
|
|
61
|
+
*/
|
|
62
|
+
async updateBusiness(id: string, businessData: BusinessUpdateRequestDTO): Promise<BusinessDTO> {
|
|
63
|
+
return this.apiClient.put<BusinessDTO>(`/business/admin/${id}`, businessData);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* ADMIN: Toggle business active status
|
|
68
|
+
*/
|
|
69
|
+
async toggleBusinessActive(id: string, isActive: boolean): Promise<BusinessDTO> {
|
|
70
|
+
return this.apiClient.put<BusinessDTO>(`/business/admin/activate/${id}`, { isActive });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"allowSyntheticDefaultImports": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"outDir": "dist",
|
|
11
|
+
"rootDir": "src",
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"lib": ["ES2022", "DOM"]
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @explorins/pers-sdk-business
|
|
3
|
+
*
|
|
4
|
+
* Platform-agnostic Business Domain SDK for PERS ecosystem
|
|
5
|
+
* Focuses on non-admin business operations
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// API Layer
|
|
9
|
+
export { BusinessApi } from './api/business-api';
|
|
10
|
+
|
|
11
|
+
// Service Layer
|
|
12
|
+
export { BusinessService } from './services/business-service';
|
|
13
|
+
|
|
14
|
+
// Models & Types
|
|
15
|
+
export * from './models';
|
|
16
|
+
|
|
17
|
+
// Factory function for creating business SDK instance
|
|
18
|
+
import { PersApiClient } from '../core/pers-api-client';
|
|
19
|
+
import { BusinessApi } from './api/business-api';
|
|
20
|
+
import { BusinessService } from './services/business-service';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a complete Business SDK instance
|
|
24
|
+
*
|
|
25
|
+
* @param apiClient - Configured PERS API client
|
|
26
|
+
* @returns Business SDK with flattened structure for better DX
|
|
27
|
+
*/
|
|
28
|
+
export function createBusinessSDK(apiClient: PersApiClient) {
|
|
29
|
+
const businessApi = new BusinessApi(apiClient);
|
|
30
|
+
const businessService = new BusinessService(businessApi);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
// Direct access to service methods (primary interface)
|
|
34
|
+
getActiveBusinesses: () => businessService.getActiveBusinesses(),
|
|
35
|
+
getAllBusinessTypes: () => businessService.getAllBusinessTypes(),
|
|
36
|
+
getBusinessById: (businessId: string) => businessService.getBusinessById(businessId),
|
|
37
|
+
getBusinessByAccount: (accountAddress: string) => businessService.getBusinessByAccount(accountAddress),
|
|
38
|
+
getBusinessesByType: (typeId: string) => businessService.getBusinessesByType(typeId),
|
|
39
|
+
|
|
40
|
+
// Admin methods
|
|
41
|
+
getAllBusinesses: () => businessService.getAllBusinesses(),
|
|
42
|
+
createBusinessByDisplayName: (displayName: string) => businessService.createBusinessByDisplayName(displayName),
|
|
43
|
+
updateBusiness: (id: string, businessData: any) => businessService.updateBusiness(id, businessData),
|
|
44
|
+
toggleBusinessActive: (id: string, isActive: boolean) => businessService.toggleBusinessActive(id, isActive),
|
|
45
|
+
|
|
46
|
+
// Advanced access for edge cases
|
|
47
|
+
api: businessApi,
|
|
48
|
+
service: businessService
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type BusinessSDK = ReturnType<typeof createBusinessSDK>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Business Domain Models
|
|
3
|
+
*
|
|
4
|
+
* Re-exports from @explorins/pers-shared for consistency with backend
|
|
5
|
+
* and to provide a single import source for business-related types.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Core business entities
|
|
9
|
+
export type {
|
|
10
|
+
BusinessDTO,
|
|
11
|
+
BusinessTypeDTO,
|
|
12
|
+
BusinessUpdateRequestDTO
|
|
13
|
+
} from '@explorins/pers-shared';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { BusinessApi } from '../api/business-api';
|
|
2
|
+
import {
|
|
3
|
+
BusinessDTO,
|
|
4
|
+
BusinessTypeDTO,
|
|
5
|
+
BusinessUpdateRequestDTO
|
|
6
|
+
} from '../models';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Platform-Agnostic Business Service
|
|
10
|
+
*
|
|
11
|
+
* Contains business logic and operations that work across platforms.
|
|
12
|
+
* No framework dependencies - pure TypeScript business logic.
|
|
13
|
+
*
|
|
14
|
+
* Focuses only on actual backend capabilities.
|
|
15
|
+
*/
|
|
16
|
+
export class BusinessService {
|
|
17
|
+
constructor(private businessApi: BusinessApi) {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get all active businesses
|
|
21
|
+
*/
|
|
22
|
+
async getActiveBusinesses(): Promise<BusinessDTO[]> {
|
|
23
|
+
return this.businessApi.getActiveBusinesses();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Get all business types
|
|
28
|
+
*/
|
|
29
|
+
async getAllBusinessTypes(): Promise<BusinessTypeDTO[]> {
|
|
30
|
+
return this.businessApi.getAllBusinessTypes();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get business by ID
|
|
35
|
+
*/
|
|
36
|
+
async getBusinessById(businessId: string): Promise<BusinessDTO> {
|
|
37
|
+
return this.businessApi.getBusinessById(businessId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get business by account address
|
|
42
|
+
*/
|
|
43
|
+
async getBusinessByAccount(accountAddress: string): Promise<BusinessDTO> {
|
|
44
|
+
return this.businessApi.getBusinessByAccount(accountAddress);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get businesses by type (client-side filtering)
|
|
49
|
+
*/
|
|
50
|
+
async getBusinessesByType(typeId: string): Promise<BusinessDTO[]> {
|
|
51
|
+
const businesses = await this.getActiveBusinesses();
|
|
52
|
+
return businesses.filter(business =>
|
|
53
|
+
business.businessType && business.businessType.id === parseInt(typeId)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ==========================================
|
|
58
|
+
// ADMIN OPERATIONS
|
|
59
|
+
// ==========================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* ADMIN: Get all businesses (active and inactive)
|
|
63
|
+
*/
|
|
64
|
+
async getAllBusinesses(): Promise<BusinessDTO[]> {
|
|
65
|
+
return this.businessApi.getAllBusinesses();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* ADMIN: Create business by display name
|
|
70
|
+
*/
|
|
71
|
+
async createBusinessByDisplayName(displayName: string): Promise<BusinessDTO> {
|
|
72
|
+
return this.businessApi.createBusinessByDisplayName(displayName);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* ADMIN: Update business
|
|
77
|
+
*/
|
|
78
|
+
async updateBusiness(id: string, businessData: BusinessUpdateRequestDTO): Promise<BusinessDTO> {
|
|
79
|
+
return this.businessApi.updateBusiness(id, businessData);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* ADMIN: Toggle business active status
|
|
84
|
+
*/
|
|
85
|
+
async toggleBusinessActive(id: string, isActive: boolean): Promise<BusinessDTO> {
|
|
86
|
+
return this.businessApi.toggleBusinessActive(id, isActive);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core SDK interfaces based on actual PERS Tourism Loyalty App patterns
|
|
3
|
+
* These interfaces reflect the real DDD architecture with Lazy Facade pattern
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Core HTTP abstractions matching the app's ApiService patterns
|
|
7
|
+
export interface CoreHttpClient {
|
|
8
|
+
get<T>(url: string, options?: any): Observable<T>;
|
|
9
|
+
post<T>(url: string, body: any, options?: any): Observable<T>;
|
|
10
|
+
put<T>(url: string, body: any, options?: any): Observable<T>;
|
|
11
|
+
delete<T>(url: string, options?: any): Observable<T>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Base facade interface reflecting the app's LazyClass pattern
|
|
15
|
+
export interface CoreFacade {
|
|
16
|
+
[key: string]: any;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Environment configuration interface based on LoyaltyEnvironment
|
|
20
|
+
export interface CoreEnvironment {
|
|
21
|
+
production: boolean;
|
|
22
|
+
version: string;
|
|
23
|
+
apiVersion: string
|
|
24
|
+
apiRoot: string;
|
|
25
|
+
apiProjectKey?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// State management interface reflecting the app's Signal-based patterns
|
|
29
|
+
export interface CoreStateManager<T> {
|
|
30
|
+
state$: Observable<T>;
|
|
31
|
+
updateState(partialState: Partial<T>): void;
|
|
32
|
+
resetState(): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Event system interface for cross-platform communication
|
|
36
|
+
export interface CoreEventEmitter<T = any> {
|
|
37
|
+
emit(eventName: string, data?: T): void;
|
|
38
|
+
on(eventName: string, callback: (data?: T) => void): void;
|
|
39
|
+
off(eventName: string, callback?: (data?: T) => void): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Service factory interface for platform-specific implementations
|
|
43
|
+
export interface CoreServiceFactory {
|
|
44
|
+
createHttpClient(): CoreHttpClient;
|
|
45
|
+
createStateManager<T>(initialState: T): CoreStateManager<T>;
|
|
46
|
+
createEventEmitter<T>(): CoreEventEmitter<T>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Observable type for platform-agnostic reactive programming
|
|
50
|
+
export interface Observable<T> {
|
|
51
|
+
subscribe(observer: {
|
|
52
|
+
next?: (value: T) => void;
|
|
53
|
+
error?: (error: any) => void;
|
|
54
|
+
complete?: () => void;
|
|
55
|
+
}): { unsubscribe(): void };
|
|
56
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-agnostic HTTP client interface
|
|
3
|
+
* To be implemented by platform-specific adapters (Angular, React Native, etc.)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface HttpClient {
|
|
7
|
+
get<T>(url: string, options?: RequestOptions): Promise<T>;
|
|
8
|
+
post<T>(url: string, body: any, options?: RequestOptions): Promise<T>;
|
|
9
|
+
put<T>(url: string, body: any, options?: RequestOptions): Promise<T>;
|
|
10
|
+
delete<T>(url: string, options?: RequestOptions): Promise<T>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RequestOptions {
|
|
14
|
+
headers?: Record<string, string>;
|
|
15
|
+
params?: Record<string, string>;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface HttpResponse<T> {
|
|
20
|
+
data: T;
|
|
21
|
+
status: number;
|
|
22
|
+
headers: Record<string, string>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface AuthProvider {
|
|
2
|
+
getToken(): Promise<string | null>;
|
|
3
|
+
getProjectKey(): Promise<string | null>;
|
|
4
|
+
authType: 'admin' | 'user' | 'firebase';
|
|
5
|
+
onTokenExpired?: () => Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface RequestOptions {
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Export simple auth config for dev-friendly usage
|
|
13
|
+
export * from './simple-auth-config.interface';
|
|
14
|
+
|
|
15
|
+
// Export auth provider factory and helpers
|
|
16
|
+
export * from './create-auth-provider';
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { AuthProvider } from './auth-provider.interface';
|
|
2
|
+
import { SimpleAuthConfig } from './simple-auth-config.interface';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Creates a platform-agnostic AuthProvider from simple configuration
|
|
6
|
+
*
|
|
7
|
+
* This factory function is completely platform-agnostic and can be used
|
|
8
|
+
* across Angular, React, Vue, Node.js, or any other JavaScript environment.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Token caching with refresh support
|
|
12
|
+
* - Automatic token refresh on expiration
|
|
13
|
+
* - Configurable token providers
|
|
14
|
+
* - Platform-independent (no localStorage assumptions)
|
|
15
|
+
*
|
|
16
|
+
* @param config - Simple auth configuration
|
|
17
|
+
* @returns AuthProvider implementation
|
|
18
|
+
*/
|
|
19
|
+
export function createAuthProvider(config: SimpleAuthConfig): AuthProvider {
|
|
20
|
+
// Store current token for refresh scenarios and caching
|
|
21
|
+
let currentToken: string | null = config.token || null;
|
|
22
|
+
let isRefreshing = false; // Prevent concurrent refresh attempts
|
|
23
|
+
let refreshPromise: Promise<void> | null = null;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
authType: config.authType || 'user',
|
|
27
|
+
|
|
28
|
+
async getToken(): Promise<string | null> {
|
|
29
|
+
// If currently refreshing, wait for it to complete
|
|
30
|
+
if (isRefreshing && refreshPromise) {
|
|
31
|
+
await refreshPromise;
|
|
32
|
+
return currentToken;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Use cached current token (updated after refresh)
|
|
36
|
+
if (currentToken) {
|
|
37
|
+
return currentToken;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Custom token provider function (always fresh)
|
|
41
|
+
if (config.tokenProvider) {
|
|
42
|
+
const token = await config.tokenProvider();
|
|
43
|
+
currentToken = token; // Cache for future calls
|
|
44
|
+
return token;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// No token available
|
|
48
|
+
return null;
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
async getProjectKey(): Promise<string | null> {
|
|
52
|
+
return config.projectKey || null;
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
async onTokenExpired(): Promise<void> {
|
|
56
|
+
// Prevent concurrent refresh attempts
|
|
57
|
+
if (isRefreshing) {
|
|
58
|
+
if (refreshPromise) {
|
|
59
|
+
await refreshPromise;
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// No refresh logic provided
|
|
65
|
+
if (!config.onTokenExpired) {
|
|
66
|
+
console.warn('Token expired but no refresh logic provided');
|
|
67
|
+
currentToken = null; // Clear expired token
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Start refresh process
|
|
72
|
+
isRefreshing = true;
|
|
73
|
+
refreshPromise = (async () => {
|
|
74
|
+
try {
|
|
75
|
+
// Execute refresh logic (should update token source)
|
|
76
|
+
await config.onTokenExpired!();
|
|
77
|
+
|
|
78
|
+
// After refresh, get the new token
|
|
79
|
+
if (config.tokenProvider) {
|
|
80
|
+
const newToken = await config.tokenProvider();
|
|
81
|
+
if (newToken && newToken !== currentToken) {
|
|
82
|
+
currentToken = newToken;
|
|
83
|
+
|
|
84
|
+
// Notify about successful token refresh
|
|
85
|
+
if (config.onTokenRefreshed) {
|
|
86
|
+
config.onTokenRefreshed(newToken);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
console.warn('Token refresh completed but no new token received');
|
|
90
|
+
currentToken = null;
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
// For static token configs, clear the token since we can't refresh
|
|
94
|
+
console.warn('Token expired for static token config - clearing token');
|
|
95
|
+
currentToken = null;
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('Token refresh failed:', error);
|
|
99
|
+
currentToken = null; // Clear token on refresh failure
|
|
100
|
+
throw error; // Re-throw to let SDK handle the error
|
|
101
|
+
} finally {
|
|
102
|
+
isRefreshing = false;
|
|
103
|
+
refreshPromise = null;
|
|
104
|
+
}
|
|
105
|
+
})();
|
|
106
|
+
|
|
107
|
+
await refreshPromise;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Platform-specific localStorage token provider for browsers
|
|
114
|
+
* This is a convenience function for browser environments
|
|
115
|
+
*/
|
|
116
|
+
export function createBrowserTokenProvider(tokenKey: string = 'userJwt'): () => Promise<string | null> {
|
|
117
|
+
return async () => {
|
|
118
|
+
if (typeof localStorage !== 'undefined') {
|
|
119
|
+
return localStorage.getItem(tokenKey);
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Platform-specific environment variable token provider for Node.js
|
|
127
|
+
* This is a convenience function for Node.js environments
|
|
128
|
+
*/
|
|
129
|
+
export function createNodeTokenProvider(envVar: string = 'JWT_TOKEN'): () => Promise<string | null> {
|
|
130
|
+
return async () => {
|
|
131
|
+
if (typeof process !== 'undefined' && process.env) {
|
|
132
|
+
return process.env[envVar] || null;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple Auth Configuration Interface - Dev-Friendly Alternative
|
|
3
|
+
*
|
|
4
|
+
* Instead of implementing AuthProvider, developers can use this simple config
|
|
5
|
+
* which will be automatically converted to a full AuthProvider implementation.
|
|
6
|
+
*/
|
|
7
|
+
export interface SimpleAuthConfig {
|
|
8
|
+
authType?: 'admin' | 'user' | 'firebase';
|
|
9
|
+
token?: string;
|
|
10
|
+
tokenProvider?: () => Promise<string | null>;
|
|
11
|
+
projectKey?: string;
|
|
12
|
+
onTokenExpired?: () => Promise<void>;
|
|
13
|
+
// Advanced: for reactive token updates after refresh
|
|
14
|
+
onTokenRefreshed?: (newToken: string) => void;
|
|
15
|
+
}
|
package/src/core/core.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @explorins/pers-sdk/core - Core SDK Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Platform-agnostic PERS API client and core functionality
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Core PERS API client
|
|
8
|
+
export * from './pers-api-client';
|
|
9
|
+
|
|
10
|
+
// Configuration interfaces
|
|
11
|
+
export * from './pers-config';
|
|
12
|
+
|
|
13
|
+
// Authentication interfaces
|
|
14
|
+
export {
|
|
15
|
+
AuthProvider,
|
|
16
|
+
SimpleAuthConfig,
|
|
17
|
+
createAuthProvider,
|
|
18
|
+
createBrowserTokenProvider,
|
|
19
|
+
createNodeTokenProvider
|
|
20
|
+
} from './auth/auth-provider.interface';
|
|
21
|
+
export type { RequestOptions as AuthRequestOptions } from './auth/auth-provider.interface';
|
|
22
|
+
|
|
23
|
+
// Platform abstractions
|
|
24
|
+
export * from './abstractions/http-client';
|
|
25
|
+
|
|
26
|
+
// Main SDK class
|
|
27
|
+
export * from '../pers-sdk';
|
|
28
|
+
|
|
29
|
+
// Version
|
|
30
|
+
export const PERS_SDK_VERSION = '1.0.0-alpha.1';
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PERS API Client - Core platform-agnostic client for PERS backend
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { HttpClient, RequestOptions } from './abstractions/http-client';
|
|
6
|
+
import { PersConfig, buildApiRoot } from './pers-config';
|
|
7
|
+
|
|
8
|
+
export class PersApiClient {
|
|
9
|
+
private readonly apiRoot: string;
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
private httpClient: HttpClient,
|
|
13
|
+
private config: PersConfig
|
|
14
|
+
) {
|
|
15
|
+
// Build API root from environment and version
|
|
16
|
+
this.apiRoot = buildApiRoot(config.environment, config.apiVersion);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get request headers including auth token and project key
|
|
21
|
+
*/
|
|
22
|
+
private async getHeaders(): Promise<Record<string, string>> {
|
|
23
|
+
const headers: Record<string, string> = {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Add authentication token
|
|
28
|
+
if (this.config.authProvider) {
|
|
29
|
+
const token = await this.config.authProvider.getToken();
|
|
30
|
+
if (token) {
|
|
31
|
+
headers['Authorization'] = `Bearer ${token}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Add project key
|
|
36
|
+
if (this.config.authProvider) {
|
|
37
|
+
const projectKey = await this.config.authProvider.getProjectKey();
|
|
38
|
+
if (projectKey) {
|
|
39
|
+
headers['x-project-key'] = projectKey;
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
// Fallback to config project key if no auth provider
|
|
43
|
+
headers['x-project-key'] = this.config.apiProjectKey;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return headers;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Make a request with proper headers, auth, and error handling
|
|
51
|
+
*/
|
|
52
|
+
private async request<T>(
|
|
53
|
+
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
|
|
54
|
+
endpoint: string,
|
|
55
|
+
body?: any,
|
|
56
|
+
options?: { retryCount?: number }
|
|
57
|
+
): Promise<T> {
|
|
58
|
+
const { retryCount = 0 } = options || {};
|
|
59
|
+
const url = `${this.apiRoot}${endpoint}`;
|
|
60
|
+
|
|
61
|
+
const requestOptions: RequestOptions = {
|
|
62
|
+
headers: await this.getHeaders(),
|
|
63
|
+
timeout: this.config.timeout || 30000,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
switch (method) {
|
|
68
|
+
case 'GET':
|
|
69
|
+
return await this.httpClient.get<T>(url, requestOptions);
|
|
70
|
+
case 'POST':
|
|
71
|
+
return await this.httpClient.post<T>(url, body, requestOptions);
|
|
72
|
+
case 'PUT':
|
|
73
|
+
return await this.httpClient.put<T>(url, body, requestOptions);
|
|
74
|
+
case 'DELETE':
|
|
75
|
+
return await this.httpClient.delete<T>(url, requestOptions);
|
|
76
|
+
default:
|
|
77
|
+
throw new Error(`Unsupported HTTP method: ${method}`);
|
|
78
|
+
}
|
|
79
|
+
} catch (error: any) {
|
|
80
|
+
// Handle 401 errors with automatic token refresh
|
|
81
|
+
if (error.status === 401 && retryCount === 0 && this.config.authProvider?.onTokenExpired) {
|
|
82
|
+
try {
|
|
83
|
+
await this.config.authProvider.onTokenExpired();
|
|
84
|
+
// Retry once with refreshed token
|
|
85
|
+
return this.request<T>(method, endpoint, body, { ...options, retryCount: 1 });
|
|
86
|
+
} catch (refreshError) {
|
|
87
|
+
throw new PersApiError(
|
|
88
|
+
`Authentication refresh failed: ${refreshError}`,
|
|
89
|
+
endpoint,
|
|
90
|
+
method,
|
|
91
|
+
401
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
throw new PersApiError(
|
|
97
|
+
`PERS API request failed: ${error.message || error}`,
|
|
98
|
+
endpoint,
|
|
99
|
+
method,
|
|
100
|
+
error.status
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generic GET request
|
|
107
|
+
*/
|
|
108
|
+
async get<T>(endpoint: string): Promise<T> {
|
|
109
|
+
return this.request<T>('GET', endpoint);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Generic POST request
|
|
114
|
+
*/
|
|
115
|
+
async post<T>(endpoint: string, body?: any): Promise<T> {
|
|
116
|
+
return this.request<T>('POST', endpoint, body);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Generic PUT request
|
|
121
|
+
*/
|
|
122
|
+
async put<T>(endpoint: string, body?: any): Promise<T> {
|
|
123
|
+
return this.request<T>('PUT', endpoint, body);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Generic DELETE request
|
|
128
|
+
*/
|
|
129
|
+
async delete<T>(endpoint: string): Promise<T> {
|
|
130
|
+
return this.request<T>('DELETE', endpoint);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get current configuration
|
|
135
|
+
*/
|
|
136
|
+
getConfig(): PersConfig {
|
|
137
|
+
return this.config;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class PersApiError extends Error {
|
|
142
|
+
constructor(
|
|
143
|
+
message: string,
|
|
144
|
+
public endpoint: string,
|
|
145
|
+
public method: string,
|
|
146
|
+
public statusCode?: number
|
|
147
|
+
) {
|
|
148
|
+
super(message);
|
|
149
|
+
this.name = 'PersApiError';
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PERS SDK Configuration interfaces
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { AuthProvider } from './auth/auth-provider.interface';
|
|
6
|
+
|
|
7
|
+
export type PersEnvironment = 'development' | 'staging' | 'production';
|
|
8
|
+
export type PersApiVersion = 'v1' | 'v1.8' | 'v1.9' | 'v2';
|
|
9
|
+
|
|
10
|
+
export interface PersConfig {
|
|
11
|
+
environment: PersEnvironment;
|
|
12
|
+
apiProjectKey: string;
|
|
13
|
+
apiVersion?: PersApiVersion;
|
|
14
|
+
timeout?: number;
|
|
15
|
+
retries?: number;
|
|
16
|
+
authProvider?: AuthProvider;
|
|
17
|
+
// Internal - constructed automatically
|
|
18
|
+
readonly apiRoot?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PersAuthConfig {
|
|
22
|
+
type: 'firebase' | 'jwt' | 'none';
|
|
23
|
+
tokenProvider?: () => Promise<string | null>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Internal function to construct API root from environment
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export function buildApiRoot(environment: PersEnvironment, version: PersApiVersion = 'v2'): string {
|
|
31
|
+
const baseUrls = {
|
|
32
|
+
development: 'https://explorins-loyalty.ngrok.io',
|
|
33
|
+
staging: `https://dev.api.pers.ninja/${version}`,
|
|
34
|
+
production: `https://dev.api.pers.ninja/${version}`
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return `${baseUrls[environment]}`;
|
|
38
|
+
}
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @explorins/pers-sdk/core - Core SDK Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Platform-agnostic PERS API client and core functionality
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Core PERS API client
|
|
8
|
+
export * from './core/pers-api-client';
|
|
9
|
+
|
|
10
|
+
// Configuration interfaces
|
|
11
|
+
export * from './core/pers-config';
|
|
12
|
+
|
|
13
|
+
// Authentication interfaces
|
|
14
|
+
export {
|
|
15
|
+
AuthProvider,
|
|
16
|
+
SimpleAuthConfig,
|
|
17
|
+
createAuthProvider,
|
|
18
|
+
createBrowserTokenProvider,
|
|
19
|
+
createNodeTokenProvider
|
|
20
|
+
} from './core/auth/auth-provider.interface';
|
|
21
|
+
export type { RequestOptions as AuthRequestOptions } from './core/auth/auth-provider.interface';
|
|
22
|
+
|
|
23
|
+
// Platform abstractions
|
|
24
|
+
export * from './core/abstractions/http-client';
|
|
25
|
+
|
|
26
|
+
// Main SDK class
|
|
27
|
+
export * from './pers-sdk';
|
|
28
|
+
|
|
29
|
+
// Version
|
|
30
|
+
export const PERS_SDK_VERSION = '1.0.0-alpha.1';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @explorins/pers-sdk - Main Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Platform-agnostic PERS SDK - Core and Business domains only
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Re-export everything from core
|
|
8
|
+
export * from './core/core';
|
|
9
|
+
|
|
10
|
+
// Re-export everything from business domain
|
|
11
|
+
export * from './business';
|
|
12
|
+
|
|
13
|
+
// NOTE: Angular integration available in separate package '@explorins/pers-sdk-angular'
|
|
14
|
+
// Future domains will be added here
|
|
15
|
+
// export * from './campaign';
|
|
16
|
+
// export * from './challenge';
|
package/src/pers-sdk.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PERS SDK - Minimal platform-agnostic client with built-in authentication
|
|
3
|
+
* Authentication is now handled at the SDK core level for better scalability
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { HttpClient } from './core/abstractions/http-client';
|
|
7
|
+
import { PersConfig } from './core/pers-config';
|
|
8
|
+
import { PersApiClient } from './core/pers-api-client';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Minimal PERS SDK - API client with authentication built-in
|
|
12
|
+
* Platform adapters provide auth providers and HTTP clients
|
|
13
|
+
*/
|
|
14
|
+
export class PersSDK {
|
|
15
|
+
private apiClient: PersApiClient;
|
|
16
|
+
|
|
17
|
+
constructor(httpClient: HttpClient, config: PersConfig) {
|
|
18
|
+
this.apiClient = new PersApiClient(httpClient, config);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the API client for direct PERS API calls
|
|
23
|
+
* This is the main interface - keep it simple!
|
|
24
|
+
*/
|
|
25
|
+
api(): PersApiClient {
|
|
26
|
+
return this.apiClient;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Quick config check
|
|
31
|
+
*/
|
|
32
|
+
isProduction(): boolean {
|
|
33
|
+
return this.apiClient.getConfig().environment === 'production';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Simple factory function
|
|
39
|
+
*/
|
|
40
|
+
export function createPersSDK(
|
|
41
|
+
httpClient: HttpClient,
|
|
42
|
+
config: PersConfig
|
|
43
|
+
): PersSDK {
|
|
44
|
+
return new PersSDK(httpClient, config);
|
|
45
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"declarationMap": true,
|
|
12
|
+
"sourceMap": true,
|
|
13
|
+
"outDir": "./dist",
|
|
14
|
+
"baseUrl": ".",
|
|
15
|
+
"paths": {
|
|
16
|
+
"@explorins/pers-shared": ["../../framework/entities"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"include": [
|
|
20
|
+
"src/**/*"
|
|
21
|
+
],
|
|
22
|
+
"exclude": [
|
|
23
|
+
"node_modules",
|
|
24
|
+
"dist",
|
|
25
|
+
"**/*.test.ts",
|
|
26
|
+
"**/*.spec.ts"
|
|
27
|
+
]
|
|
28
|
+
}
|