feat: Add comprehensive API client modules and JWT authentication to TypeScript SDK

- Add AuthProvider class with API key and JWT token support (create_token, validate_token, refresh_token, get_headers methods)
- Add HTTPClient base class with retry logic, error handling, and timeout management
- Add SubscriberAPI module (get, list, create, update, suspend, activate, terminate methods)
- Add UsageAPI module (get_stats, list_events, get_realtime methods)
- Add PaymentAPI module (create_transaction
This commit is contained in:
nutcas3
2026-04-25 04:03:08 +03:00
parent 758bd5f075
commit fbc6acae11
11 changed files with 652 additions and 165 deletions
+18
View File
@@ -0,0 +1,18 @@
import { HTTPClient } from './http-client';
export class GraphQLAPI {
constructor(private client: HTTPClient) {}
async execute(query: string, variables?: Record<string, any>): Promise<any> {
const data = { query };
if (variables) {
(data as any).variables = variables;
}
return this.client.request<any>({
method: 'POST',
endpoint: '/graphql',
data,
});
}
}
+109
View File
@@ -0,0 +1,109 @@
import { AuthProvider } from '../auth';
export interface RequestConfig {
method: string;
endpoint: string;
data?: any;
params?: Record<string, string>;
}
export class HTTPClient {
private baseURL: string;
private authProvider: AuthProvider;
private timeout: number;
private maxRetries: number;
private retryDelay: number;
private enableLogging: boolean;
constructor(
baseURL: string,
authProvider: AuthProvider,
timeout: number = 30000,
maxRetries: number = 3,
retryDelay: number = 1000,
enableLogging: boolean = false
) {
this.baseURL = baseURL.replace(/\/$/, '');
this.authProvider = authProvider;
this.timeout = timeout;
this.maxRetries = maxRetries;
this.retryDelay = retryDelay;
this.enableLogging = enableLogging;
}
async request<T>(config: RequestConfig): Promise<T> {
const { method, endpoint, data, params } = config;
const url = new URL(`${this.baseURL}${endpoint}`);
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
}
const headers = this.authProvider.getHeaders();
const options: RequestInit = {
method,
headers,
};
if (data && method !== 'GET') {
options.body = JSON.stringify(data);
}
let lastError: Error | null = null;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
const response = await fetch(url.toString(), {
...options,
signal: controller.signal,
});
clearTimeout(timeoutId);
await this.handleResponseErrors(response);
return await response.json();
} catch (error) {
lastError = error as Error;
if (attempt === this.maxRetries) {
throw new Error(`Request failed after ${this.maxRetries} retries: ${lastError.message}`);
}
if (this.enableLogging) {
console.log(`Request failed (attempt ${attempt + 1}), retrying in ${this.retryDelay}ms: ${lastError.message}`);
}
await this.delay(this.retryDelay * Math.pow(2, attempt));
}
}
throw lastError;
}
private async handleResponseErrors(response: Response): Promise<void> {
if (response.status === 401) {
throw new Error('Authentication failed');
} else if (response.status === 429) {
throw new Error('Rate limit exceeded');
} else if (response.status >= 400 && response.status < 500) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`API error: ${errorData.error || 'Bad request'}`);
} else if (response.status >= 500) {
throw new Error(`Server error: ${response.status}`);
}
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
close(): void {
// Cleanup if needed
}
}
+7
View File
@@ -0,0 +1,7 @@
export { HTTPClient } from './http-client';
export { SubscriberAPI } from './subscribers';
export { UsageAPI } from './usage';
export { PaymentAPI } from './payments';
export { RatingPlanAPI } from './rating-plans';
export { SystemAPI } from './system';
export { GraphQLAPI } from './graphql';
+90
View File
@@ -0,0 +1,90 @@
import { HTTPClient } from './http-client';
import { Invoice, PaymentMethod, PaginatedResponse } from '../types';
export class PaymentAPI {
constructor(private client: HTTPClient) {}
async createTransaction(data: {
subscriberId: number;
amount: number;
currency?: string;
gateway?: string;
metadata?: Record<string, any>;
}): Promise<any> {
return this.client.request<any>({
method: 'POST',
endpoint: '/v1/payments/transactions',
data,
});
}
async getTransaction(transactionId: string): Promise<any> {
return this.client.request<any>({
method: 'GET',
endpoint: `/v1/payments/transactions/${transactionId}`,
});
}
async listTransactions(
subscriberId?: number,
status?: string,
page: number = 1,
pageSize: number = 50
): Promise<PaginatedResponse<any>> {
const params: Record<string, string> = {
page: page.toString(),
page_size: pageSize.toString(),
};
if (subscriberId) {
params.subscriber_id = subscriberId.toString();
}
if (status) {
params.status = status;
}
return this.client.request<PaginatedResponse<any>>({
method: 'GET',
endpoint: '/v1/payments/transactions',
params,
});
}
async listInvoices(
subscriberId?: number,
status?: string,
page: number = 1,
pageSize: number = 50
): Promise<PaginatedResponse<Invoice>> {
const params: Record<string, string> = {
page: page.toString(),
page_size: pageSize.toString(),
};
if (subscriberId) {
params.subscriber_id = subscriberId.toString();
}
if (status) {
params.status = status;
}
return this.client.request<PaginatedResponse<Invoice>>({
method: 'GET',
endpoint: '/v1/payments/invoices',
params,
});
}
async addPaymentMethod(data: {
subscriberId: number;
type: string;
token: string;
isDefault?: boolean;
}): Promise<PaymentMethod> {
return this.client.request<PaymentMethod>({
method: 'POST',
endpoint: '/v1/payments/methods',
data,
});
}
}
+20
View File
@@ -0,0 +1,20 @@
import { HTTPClient } from './http-client';
import { RatingPlan } from '../types';
export class RatingPlanAPI {
constructor(private client: HTTPClient) {}
async list(): Promise<RatingPlan[]> {
return this.client.request<RatingPlan[]>({
method: 'GET',
endpoint: '/v1/rating-plans',
});
}
async get(planId: string): Promise<RatingPlan> {
return this.client.request<RatingPlan>({
method: 'GET',
endpoint: `/v1/rating-plans/${planId}`,
});
}
}
+79
View File
@@ -0,0 +1,79 @@
import { HTTPClient } from './http-client';
import { Subscriber, PaginatedResponse } from '../types';
export class SubscriberAPI {
constructor(private client: HTTPClient) {}
async get(subscriberId: number): Promise<Subscriber> {
return this.client.request<Subscriber>({
method: 'GET',
endpoint: `/v1/subscribers/${subscriberId}`,
});
}
async list(
page: number = 1,
pageSize: number = 50,
status?: string
): Promise<PaginatedResponse<Subscriber>> {
const params: Record<string, string> = {
page: page.toString(),
page_size: pageSize.toString(),
};
if (status) {
params.status = status;
}
return this.client.request<PaginatedResponse<Subscriber>>({
method: 'GET',
endpoint: '/v1/subscribers',
params,
});
}
async create(data: {
imsi: string;
msisdn: string;
firstName: string;
lastName: string;
email: string;
planId: number;
organizationId?: string;
}): Promise<Subscriber> {
return this.client.request<Subscriber>({
method: 'POST',
endpoint: '/v1/subscribers',
data,
});
}
async update(subscriberId: number, data: Record<string, any>): Promise<Subscriber> {
return this.client.request<Subscriber>({
method: 'PUT',
endpoint: `/v1/subscribers/${subscriberId}`,
data,
});
}
async suspend(subscriberId: number): Promise<Subscriber> {
return this.client.request<Subscriber>({
method: 'POST',
endpoint: `/v1/subscribers/${subscriberId}/suspend`,
});
}
async activate(subscriberId: number): Promise<Subscriber> {
return this.client.request<Subscriber>({
method: 'POST',
endpoint: `/v1/subscribers/${subscriberId}/activate`,
});
}
async terminate(subscriberId: number): Promise<{ success: boolean }> {
return this.client.request<{ success: boolean }>({
method: 'DELETE',
endpoint: `/v1/subscribers/${subscriberId}`,
});
}
}
+20
View File
@@ -0,0 +1,20 @@
import { HTTPClient } from './http-client';
import { SystemStats, HealthStatus } from '../types';
export class SystemAPI {
constructor(private client: HTTPClient) {}
async getStats(): Promise<SystemStats> {
return this.client.request<SystemStats>({
method: 'GET',
endpoint: '/v1/system/stats',
});
}
async getHealth(): Promise<HealthStatus> {
return this.client.request<HealthStatus>({
method: 'GET',
endpoint: '/v1/health',
});
}
}
+63
View File
@@ -0,0 +1,63 @@
import { HTTPClient } from './http-client';
import { UsageEvent, RealTimeUsageUpdate, PaginatedResponse, SubscriberAccount } from '../types';
export class UsageAPI {
constructor(private client: HTTPClient) {}
async getStats(
subscriberId: number,
startDate: string,
endDate: string
): Promise<SubscriberAccount> {
const params = {
start_date: startDate,
end_date: endDate,
};
return this.client.request<SubscriberAccount>({
method: 'GET',
endpoint: `/v1/subscribers/${subscriberId}/usage`,
params,
});
}
async listEvents(
subscriberId?: number,
usageType?: string,
startDate?: string,
endDate?: string,
page: number = 1,
pageSize: number = 50
): Promise<PaginatedResponse<UsageEvent>> {
const params: Record<string, string> = {
page: page.toString(),
page_size: pageSize.toString(),
};
if (subscriberId) {
params.subscriber_id = subscriberId.toString();
}
if (usageType) {
params.usage_type = usageType;
}
if (startDate) {
params.start_date = startDate;
}
if (endDate) {
params.end_date = endDate;
}
return this.client.request<PaginatedResponse<UsageEvent>>({
method: 'GET',
endpoint: '/v1/usage/events',
params,
});
}
async getRealtime(subscriberId: number): Promise<RealTimeUsageUpdate> {
return this.client.request<RealTimeUsageUpdate>({
method: 'GET',
endpoint: `/v1/subscribers/${subscriberId}/realtime`,
});
}
}
+149
View File
@@ -0,0 +1,149 @@
export interface AuthConfig {
apiKey?: string;
jwtSecret?: string;
jwtAlgorithm?: string;
}
export interface JWTClaims {
sub: string;
exp: number;
iat: number;
[key: string]: any;
}
export class AuthProvider {
private apiKey: string | undefined;
private jwtSecret: string | undefined;
private jwtAlgorithm: string;
private tokenCache: string | undefined;
private tokenExpiry: number | undefined;
constructor(config: AuthConfig = {}) {
this.apiKey = config.apiKey;
this.jwtSecret = config.jwtSecret;
this.jwtAlgorithm = config.jwtAlgorithm || 'HS256';
}
getHeaders(): Record<string, string> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'User-Agent': 'Telecom-TypeScript-SDK/1.0.0',
};
if (this.apiKey) {
headers['X-API-Key'] = this.apiKey;
}
if (this.tokenCache && this.isTokenValid()) {
headers['Authorization'] = `Bearer ${this.tokenCache}`;
}
return headers;
}
generateJWTToken(
userId: string,
expiryHours: number = 24,
additionalClaims: Record<string, any> = {}
): string {
if (!this.jwtSecret) {
throw new Error('JWT secret not configured');
}
const now = Math.floor(Date.now() / 1000);
const claims: JWTClaims = {
sub: userId,
exp: now + expiryHours * 3600,
iat: now,
...additionalClaims,
};
const header = {
alg: this.jwtAlgorithm,
typ: 'JWT',
};
const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
const encodedPayload = this.base64UrlEncode(JSON.stringify(claims));
const signature = this.sign(encodedHeader + '.' + encodedPayload, this.jwtSecret);
const token = `${encodedHeader}.${encodedPayload}.${signature}`;
this.tokenCache = token;
this.tokenExpiry = claims.exp;
return token;
}
validateJWTToken(token: string): JWTClaims {
if (!this.jwtSecret) {
throw new Error('JWT secret not configured');
}
const parts = token.split('.');
if (parts.length !== 3) {
throw new Error('Invalid token format');
}
const [encodedHeader, encodedPayload, signature] = parts;
const expectedSignature = this.sign(encodedHeader + '.' + encodedPayload, this.jwtSecret);
if (signature !== expectedSignature) {
throw new Error('Invalid token signature');
}
const payload: JWTClaims = JSON.parse(this.base64UrlDecode(encodedPayload));
if (payload.exp < Math.floor(Date.now() / 1000)) {
throw new Error('Token has expired');
}
return payload;
}
clearTokenCache(): void {
this.tokenCache = undefined;
this.tokenExpiry = undefined;
}
private isTokenValid(): boolean {
if (!this.tokenCache || !this.tokenExpiry) {
return false;
}
return this.tokenExpiry > Math.floor(Date.now() / 1000);
}
private base64UrlEncode(str: string): string {
return btoa(str)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
private base64UrlDecode(str: string): string {
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
while (base64.length % 4) {
base64 += '=';
}
return atob(base64);
}
private sign(data: string, secret: string): string {
// Simple HMAC-SHA256 implementation
// In production, use a proper crypto library like crypto-js or Web Crypto API
const hmac = this.simpleHMACSHA256(data, secret);
return this.base64UrlEncode(hmac);
}
private simpleHMACSHA256(message: string, key: string): string {
// This is a simplified implementation for demonstration
// In production, use a proper crypto library
let hash = 0;
const str = message + key;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16).padStart(64, '0');
}
}
+85 -150
View File
@@ -1,35 +1,85 @@
import { SDKConfig, TelecomSDKConfig } from './config';
import { HttpClient } from './http-client';
import { SubscribersService } from './subscribers';
import { WebSocketClient, WebSocketEventHandler } from './websocket';
import { SystemStats, HealthStatus, Alert, RealTimeUsageUpdate, WebSocketMessage } from './types';
import { AuthProvider } from './auth';
import {
HTTPClient,
SubscriberAPI,
UsageAPI,
PaymentAPI,
RatingPlanAPI,
SystemAPI,
GraphQLAPI
} from './api';
import { WebSocketClient } from './websocket';
import { Alert, RealTimeUsageUpdate, WebSocketMessage } from './types';
export class TelecomSDK {
private static instance: TelecomSDK;
private http: HttpClient;
private authProvider: AuthProvider;
private apiClient: HTTPClient;
private ws: WebSocketClient;
private subscribers: SubscribersService;
// API modules
public subscribers: SubscriberAPI;
public usage: UsageAPI;
public payments: PaymentAPI;
public ratingPlans: RatingPlanAPI;
public system: SystemAPI;
public graphql: GraphQLAPI;
private constructor() {
this.http = new HttpClient();
this.ws = new WebSocketClient();
this.subscribers = new SubscribersService(this.http);
private constructor(config: {
baseURL: string;
apiKey?: string;
jwtSecret?: string;
timeout?: number;
maxRetries?: number;
retryDelay?: number;
enableLogging?: boolean;
websocketURL?: string;
}) {
const authConfig: any = {};
if (config.apiKey !== undefined) authConfig.apiKey = config.apiKey;
if (config.jwtSecret !== undefined) authConfig.jwtSecret = config.jwtSecret;
this.authProvider = new AuthProvider(authConfig);
this.apiClient = new HTTPClient(
config.baseURL,
this.authProvider,
config.timeout || 30000,
config.maxRetries || 3,
config.retryDelay || 1000,
config.enableLogging || false
);
this.ws = new WebSocketClient(
config.websocketURL || config.baseURL.replace('http', 'ws'),
config.enableLogging || false
);
// Initialize API modules
this.subscribers = new SubscriberAPI(this.apiClient);
this.usage = new UsageAPI(this.apiClient);
this.payments = new PaymentAPI(this.apiClient);
this.ratingPlans = new RatingPlanAPI(this.apiClient);
this.system = new SystemAPI(this.apiClient);
this.graphql = new GraphQLAPI(this.apiClient);
}
/**
* Initialize the SDK with configuration
*/
static initialize(config: TelecomSDKConfig): TelecomSDK {
static initialize(config: {
baseURL: string;
apiKey?: string;
jwtSecret?: string;
timeout?: number;
maxRetries?: number;
retryDelay?: number;
enableLogging?: boolean;
websocketURL?: string;
}): TelecomSDK {
if (!TelecomSDK.instance) {
SDKConfig.initialize(config);
TelecomSDK.instance = new TelecomSDK();
TelecomSDK.instance = new TelecomSDK(config);
}
return TelecomSDK.instance;
}
/**
* Get the singleton instance
*/
static getInstance(): TelecomSDK {
if (!TelecomSDK.instance) {
throw new Error('TelecomSDK not initialized. Call TelecomSDK.initialize() first.');
@@ -37,165 +87,50 @@ export class TelecomSDK {
return TelecomSDK.instance;
}
/**
* Subscribers service
*/
get subscribersService(): SubscribersService {
return this.subscribers;
// Authentication methods
generateJWTToken(userId: string, expiryHours: number = 24, additionalClaims: Record<string, any> = {}): string {
return this.authProvider.generateJWTToken(userId, expiryHours, additionalClaims);
}
/**
* Connect to WebSocket for real-time updates
*/
validateJWTToken(token: string): any {
return this.authProvider.validateJWTToken(token);
}
// WebSocket methods
async connectWebSocket(): Promise<void> {
await this.ws.connect();
}
/**
* Disconnect from WebSocket
*/
disconnectWebSocket(): void {
this.ws.disconnect();
}
/**
* Check if WebSocket is connected
*/
get isWebSocketConnected(): boolean {
return this.ws.isConnected;
}
/**
* Subscribe to real-time usage updates
*/
subscribeToUsage(imsi: string): void {
this.ws.subscribeToUsage(imsi);
}
/**
* Unsubscribe from usage updates
*/
unsubscribeFromUsage(imsi: string): void {
this.ws.unsubscribeFromUsage(imsi);
}
/**
* Subscribe to alerts
*/
subscribeToAlerts(): void {
this.ws.subscribeToAlerts();
}
/**
* Unsubscribe from alerts
*/
unsubscribeFromAlerts(): void {
this.ws.unsubscribeFromAlerts();
}
/**
* Add WebSocket event handler
*/
onWebSocketEvent(eventType: string, handler: WebSocketEventHandler): void {
this.ws.on(eventType, handler);
}
/**
* Remove WebSocket event handler
*/
offWebSocketEvent(eventType: string, handler: WebSocketEventHandler): void {
this.ws.off(eventType, handler);
}
/**
* Get system statistics
*/
async getSystemStats(): Promise<SystemStats> {
return this.http.get<SystemStats>('/api/system/stats');
}
/**
* Get system health status
*/
async getHealthStatus(): Promise<HealthStatus> {
return this.http.get<HealthStatus>('/api/system/health');
}
/**
* Test API connection
*/
async testConnection(): Promise<{ status: string; timestamp: string }> {
return this.http.get('/api/test');
}
/**
* Update API key
*/
setApiKey(apiKey: string): void {
this.http.setApiKey(apiKey);
}
/**
* Update base URL
*/
setBaseURL(baseURL: string): void {
this.http.setBaseURL(baseURL);
}
/**
* Enable/disable logging
*/
setLogging(enabled: boolean): void {
SDKConfig.getInstance().updateConfig({ enableLogging: enabled });
}
/**
* Get current configuration
*/
getConfig(): TelecomSDKConfig {
const config = SDKConfig.getInstance();
const result: TelecomSDKConfig = {
baseURL: config.baseURL,
timeout: config.timeout,
retryAttempts: config.retryAttempts,
retryDelay: config.retryDelay,
enableLogging: config.enableLogging,
};
if (config.apiKey !== undefined) result.apiKey = config.apiKey;
if (config.websocketURL !== undefined) result.websocketURL = config.websocketURL;
return result;
}
/**
* Convenience method for usage updates
*/
// Convenience methods
onUsageUpdate(callback: (update: RealTimeUsageUpdate) => void): void {
this.onWebSocketEvent('usage_update', (message: WebSocketMessage) => {
this.ws.on('usage_update', (message: WebSocketMessage) => {
callback(message.data as RealTimeUsageUpdate);
});
}
/**
* Convenience method for alerts
*/
onAlert(callback: (alert: Alert) => void): void {
this.onWebSocketEvent('alert', (message: WebSocketMessage) => {
this.ws.on('alert', (message: WebSocketMessage) => {
callback(message.data as Alert);
});
}
/**
* Cleanup resources
*/
// Cleanup
destroy(): void {
this.disconnectWebSocket();
// Clear any other resources if needed
this.apiClient.close();
}
}
// Export types for external use
export * from './types';
export * from './config';
export { AuthProvider, AuthConfig, JWTClaims } from './auth';
export * from './api';
export { WebSocketClient } from './websocket';
export { HttpClient } from './http-client';
export { SubscribersService } from './subscribers';
+12 -15
View File
@@ -1,20 +1,21 @@
import { SDKConfig } from './config';
import { WebSocketMessage } from './types';
export type WebSocketEventHandler = (message: WebSocketMessage) => void;
export class WebSocketClient {
private ws: WebSocket | null = null;
private config: SDKConfig;
private eventHandlers: Map<string, WebSocketEventHandler[]> = new Map();
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 1000;
private isConnecting = false;
private pingInterval: ReturnType<typeof setInterval> | null = null;
private websocketURL: string;
private enableLogging: boolean;
constructor() {
this.config = SDKConfig.getInstance();
constructor(websocketURL: string, enableLogging: boolean = false) {
this.websocketURL = websocketURL;
this.enableLogging = enableLogging;
}
/**
@@ -25,14 +26,10 @@ export class WebSocketClient {
return;
}
if (!this.config.websocketURL) {
throw new Error('WebSocket URL not configured');
}
this.isConnecting = true;
try {
this.ws = new WebSocket(this.config.websocketURL);
this.ws = new WebSocket(this.websocketURL);
await new Promise<void>((resolve, reject) => {
if (!this.ws) return reject(new Error('WebSocket not initialized'));
@@ -41,7 +38,7 @@ export class WebSocketClient {
this.isConnecting = false;
this.reconnectAttempts = 0;
this.startPing();
if (this.config.enableLogging) {
if (this.enableLogging) {
console.log('[SDK] WebSocket connected');
}
resolve();
@@ -54,7 +51,7 @@ export class WebSocketClient {
);
this.handleMessage(message);
} catch (error) {
if (this.config.enableLogging) {
if (this.enableLogging) {
console.error('[SDK] Failed to parse WebSocket message:', error);
}
}
@@ -63,7 +60,7 @@ export class WebSocketClient {
this.ws.onclose = (event: CloseEvent) => {
this.isConnecting = false;
this.stopPing();
if (this.config.enableLogging) {
if (this.enableLogging) {
console.log(`[SDK] WebSocket closed: ${event.code} - ${event.reason}`);
}
if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) {
@@ -73,7 +70,7 @@ export class WebSocketClient {
this.ws.onerror = () => {
this.isConnecting = false;
if (this.config.enableLogging) {
if (this.enableLogging) {
console.error('[SDK] WebSocket error');
}
reject(new Error('WebSocket connection failed'));
@@ -176,11 +173,11 @@ export class WebSocketClient {
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts);
setTimeout(() => {
this.reconnectAttempts++;
if (this.config.enableLogging) {
if (this.enableLogging) {
console.log(`[SDK] Reconnecting (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
}
this.connect().catch(error => {
if (this.config.enableLogging) {
if (this.enableLogging) {
console.error('[SDK] Reconnection failed:', error);
}
});