/** * Luqta SDK Client * * Main client class for the Luqta SDK supporting two modes: * 1. Custom UI Mode - API-only integration for developers building their own UI * 2. Pre-configured UI Mode - Complete UI with customizable branding * * And two API access types: * 1. Application APIs - Only require API Key + App ID (public data) * 2. End-User APIs - Require API Key + App ID + User identification */ import { LuqtaConfig, LegacyConfig, RequestOptions, ApiResponse, UserProfile, UserIdentification, BrandingConfig, UIConfig, Contest, PaginatedResponse } from './types'; /** * Main Luqta SDK Client * * Supports two modes of operation: * - Custom Mode: API-only integration for custom UI implementations * - Pre-configured Mode: Complete UI with branding customization * * And two API authentication types: * - Application APIs: Only need apiKey + appId (for public data like contest listings) * - End-User APIs: Need apiKey + appId + user identification (for user-specific actions) * * @example Custom Mode with End-User APIs: * ```typescript * const client = new LuqtaClient({ * mode: 'custom', * apiKey: 'your-api-key', * appId: 'your-app-id', * user: { email: 'user@example.com' } * }); * * await client.initializeUser(); * const contests = await client.contests.getAll(); * await client.contests.participate(123); * ``` * * @example Custom Mode with Application-Only APIs: * ```typescript * // No user required for public data * const client = new LuqtaClient({ * mode: 'custom', * apiKey: 'your-api-key', * appId: 'your-app-id' * }); * * // These work without user initialization * const contests = await client.getPublicContests(); * const categories = await client.getCategories(); * ``` * * @example Pre-configured Mode (Complete UI): * ```typescript * const client = new LuqtaClient({ * mode: 'preconfigured', * apiKey: 'your-api-key', * appId: 'your-app-id', * containerId: 'luqta-container', * user: { email: 'user@example.com' }, * branding: { * primaryColor: '#5304fb', * logoUrl: 'https://example.com/logo.png' * } * }); * * await client.render(); * ``` */ export declare class LuqtaClient { private apiKey; private appId; private baseURL; private timeout; private production; private defaultHeaders; private mode; private user?; private isUserInitialized; private containerId?; private branding?; private locale; private rtl; private screens; private onAction?; private onError?; private showProfileIcon; private isSdkInitialized; private zeroStorage; private userProfile; private readonly SDK_AUTH_TOKEN_KEY; private readonly USER_AUTH_TOKEN_KEY; private readonly EMAIL_PATTERN; private readonly PHONE_PATTERN; private readonly RATE_LIMIT_WINDOW_MS; private readonly MAX_REQUESTS_PER_WINDOW; private readonly MAX_REQUESTS_PER_ENDPOINT; private readonly BURST_LIMIT; private readonly BURST_WINDOW_MS; private readonly LOCKOUT_DURATION_MS; private readonly MAX_VIOLATIONS_BEFORE_LOCKOUT; private requestTimestamps; private endpointRequestCounts; private burstTimestamps; private violationCount; private lockoutUntil; private readonly DEDUP_WINDOW_MS; private recentRequests; private readonly TOKEN_FINGERPRINT_KEY; private sessionFingerprint; private securityEvents; private readonly MAX_SECURITY_EVENTS; readonly contests: ContestAPI; readonly levels: LevelAPI; readonly quiz: QuizAPI; readonly rewards: RewardAPI; readonly notifications: NotificationAPI; readonly profile: ProfileAPI; private uiRenderer?; /** * Creates a new LuqtaClient instance */ constructor(config: LuqtaConfig | LegacyConfig); /** * Normalizes legacy config to new format */ private normalizeConfig; /** * Validates base configuration */ private validateBaseConfig; /** * Validates user configuration * At least one of email, phone_number, username, or uuid is required */ private validateUserConfig; /** * Constructs base URL based on environment */ private constructBaseURL; /** * Generates a unique session fingerprint for token integrity */ private generateSessionFingerprint; /** * Initializes security features */ private initializeSecurity; /** * Logs security events for monitoring */ private logSecurityEvent; /** * Checks if client is currently locked out due to abuse */ private isLockedOut; /** * Records a security violation and potentially locks out the client */ private recordViolation; /** * Cleans up old timestamps for rate limiting */ private cleanupOldSecurityData; /** * Checks rate limits before making a request * @throws {LuqtaError} If rate limit exceeded */ private checkRateLimit; /** * Checks for duplicate requests (prevents accidental double submissions) * @returns true if request is a duplicate */ private isDuplicateRequest; /** * Validates and sanitizes request payload * Prevents injection attacks and malformed data */ private sanitizePayload; /** * Validates token integrity * Checks if token has been tampered with * Supports both JWT (3 parts) and JWE (5 parts) token formats */ private validateTokenIntegrity; /** * Validates numeric parameters to prevent injection */ private validateNumericParam; /** * Validates string parameters to prevent injection */ private validateStringParam; /** * Performs security checks before making an API request * This is the main security gate for all API calls */ private performSecurityChecks; /** * Clears SDK auth token */ private clearSdkAuthToken; /** * Clears user auth token */ private clearUserAuthToken; /** * Clears user auth token (call on logout) */ clearUserToken(): void; /** * Gets security status for debugging/monitoring */ getSecurityStatus(): { isLockedOut: boolean; lockoutRemaining: number; violationCount: number; recentRequestCount: number; recentEvents: Array<{ type: string; timestamp: number; details: string; }>; }; /** * Resets security state (for testing purposes only) * WARNING: This should not be used in production */ resetSecurityState(): void; /** * Initializes the SDK with API key and App ID * This is automatically called and does NOT require any user information * Only requires apiKey and appId which are passed in the config * * After successful initialization, it fetches contests to validate SDK access * * @returns Promise with initialization result including contests * @throws {LuqtaError} If initialization fails */ initializeSdk(): Promise<{ success: boolean; contests: PaginatedResponse; }>; /** * Fetches contests with server-side pagination * Uses SDK auth token from initialization * * @param params - Pagination parameters (page, per_page) * @returns Paginated contests response * @throws {LuqtaError} If no contests exist or fetch fails */ fetchContests(params?: { page?: number; per_page?: number; }): Promise>; /** * Checks if SDK is initialized */ isSdkReady(): boolean; /** * Configures the SDK for pre-configured UI mode * Call this after creating the client to set up UI settings and branding * * @param config - UI configuration including containerId, branding, locale, etc. * @returns The client instance for method chaining * * @example * ```typescript * const client = new LuqtaClient({ * apiKey: 'lq_dev_app_xxx', * appId: 'your-app-id' * }); * * client.configure({ * containerId: 'luqta-container', * branding: { * primaryColor: '#5304fb', * secondaryColor: '#8f67fd', * backgroundColor: '#ffffff', * textColor: '#111827', * logoUrl: 'https://example.com/logo.png', * appName: 'My App', * borderRadius: 8, * fontFamily: 'Inter, sans-serif' * }, * locale: 'en', * rtl: false, * screens: ['contests', 'quizzes', 'rewards', 'profile'], * onAction: (action) => console.log('Action:', action), * onError: (error) => console.error('Error:', error) * }); * * // Then render the UI * await client.render(); * ``` */ configure(config: UIConfig): LuqtaClient; /** * Updates branding configuration * Can be called anytime to update the UI theme * * @param branding - Branding configuration options * @returns The client instance for method chaining * * @example * ```typescript * client.setBranding({ * primaryColor: '#ff5722', * logoUrl: 'https://example.com/new-logo.png' * }); * ``` */ setBranding(branding: BrandingConfig): LuqtaClient; /** * Gets current branding configuration */ getBranding(): BrandingConfig | undefined; /** * Sets the locale for the UI * * @param locale - Language code ('en' or 'ar') * @param rtl - Enable RTL layout (auto-detected for 'ar' if not specified) * @returns The client instance for method chaining */ setLocale(locale: 'en' | 'ar', rtl?: boolean): LuqtaClient; /** * Gets current locale */ getLocale(): 'en' | 'ar'; /** * Initializes end-user session * Required before calling any end-user APIs * * Flow: * 1. Call /sdk/app/initialize/user with email or phone * 2. If returns SDK-USER-001 (user not synced), throw error asking to sync first * 3. On success, store token as userAuthorizationAccessTokenLuqtaSdk * * @throws {LuqtaError} If user config is missing or initialization fails */ initializeUser(): Promise; /** * Alias for initializeUser() - for backward compatibility * @deprecated Use initializeUser() instead */ initialize(): Promise; /** * Sets user identification for end-user APIs */ setUser(user: UserIdentification): void; /** * Checks if user is initialized */ isInitialized(): boolean; /** * True when a user auth token is persisted in cookies (e.g. from a * previous session). Useful to decide whether to send user credentials * on endpoints that accept but don't require them, even when the * in-memory init flag hasn't been set yet. */ hasUserToken(): boolean; /** * Restores in-memory init flags from persisted tokens. Call this on * app boot — if a previous session saved tokens they can be reused * without re-running `initializeSdk()` / `initializeUser()`. * Returns whether a user token was found. */ tryRestoreSession(): boolean; /** * Syncs user profile data to Luqta * Call this before initializeUser() if user doesn't exist yet * * @param userData - User profile data including name, email, phone, etc. * @returns Promise with sync result * * @example * ```typescript * await client.syncUser({ * name: 'John Doe', * email: 'john@example.com', * phone_number: '+1234567890', * gender: 'male', * country: 'Pakistan', * dob: null, * Verified: true, * policy_accept: true * }); * ``` */ syncUser(userData: UserProfile): Promise; /** * Syncs user and then initializes - convenience method * Use this when you need to sync a new user and get auth token * * @param userData - User profile data * @returns Promise resolving when user is synced and initialized */ syncAndInitializeUser(userData: UserProfile): Promise; /** * Gets all public contests (no user required) */ getPublicContests(params?: { page?: number; per_page?: number; }): Promise; /** * Gets trending contests (no user required) */ getTrendingContests(): Promise>; /** * Gets premium/VIP contests (no user required) */ getPremiumContests(): Promise>; /** * Gets recent contests (no user required) */ getRecentContests(): Promise>; /** * Gets contest details with optional user progress * Uses user token if user is initialized (to get progress), otherwise uses SDK token * * @param contestId - The contest ID to get details for * @param accessCode - Access code for private contests (optional) * @returns Contest details with levels, prizes, and user progress (if user is initialized) */ getContestDetails(contestId: number, accessCode?: string): Promise; /** * Gets categories (no user required) */ getCategories(): Promise; /** * Gets available rewards (no user required) */ getRewardsList(): Promise; /** * Gets contest details with user progress * Requires user to be initialized with auth token * * @param contestId - The contest ID to get details for * @returns Contest details with progress * @throws {LuqtaError} If user needs to participate first (ERROR_FATAL) */ getContestDetailsProgress(contestId: number): Promise; /** * Participates in a contest * Requires user to be initialized with auth token * * @param contestId - The contest ID to participate in * @param accessCode - Access code for private contests (optional) * @returns Participation result */ participateContest(contestId: number, accessCode?: string): Promise; /** * Renders the pre-configured UI * Only available in preconfigured mode * * Flow: * 1. Initialize SDK (apiKey + appId only, no user required) * 2. Fetch contests * 3. Render UI with branding config * * @example * ```typescript * const client = new LuqtaClient({ * mode: 'preconfigured', * apiKey: 'your-api-key', * appId: 'your-app-id', * containerId: 'luqta-container', * branding: { primaryColor: '#5304fb' } * }); * * await client.render(); * ``` */ render(): Promise; /** * Refreshes the UI content * Only available in preconfigured mode after render() has been called */ refreshUI(): Promise; /** * Makes an application-level request (no user auth required) * Use for: contest listings, categories, public rewards */ applicationRequest(endpoint: string, options?: RequestOptions, _isRetry?: boolean): Promise>; /** * Makes an end-user request (requires user initialization) * Use for: participation, quiz, rewards, profile, etc. */ userRequest(endpoint: string, options?: RequestOptions, _isRetry?: boolean): Promise>; /** * Detects the `{code: ERROR_FATAL, message: "The token expired."}` pattern * that the backend emits on stale bearer tokens. */ private isTokenExpired; /** Refreshes the user auth token. On sync-required errors, auto-syncs if * `userProfile` was provided in config. Returns true on success. */ private refreshUserToken; /** Refreshes the SDK auth token by re-running initializeSdk(). */ private refreshSdkToken; /** * Generic request method (for backward compatibility) * Uses user request if initialized, otherwise application request */ request(endpoint: string, options?: RequestOptions): Promise>; get(endpoint: string, params?: Record): Promise>; post(endpoint: string, body?: any): Promise>; put(endpoint: string, body?: any): Promise>; delete(endpoint: string): Promise>; patch(endpoint: string, body?: any): Promise>; private buildRequestURL; /** * Builds headers for application-level requests (API Key + App ID only) */ private buildAppHeaders; /** * Builds headers for initialization (API Key + App ID + User) */ private buildInitHeaders; /** * Builds headers for end-user requests (User Auth Token) */ private buildUserHeaders; private storeSdkAuthToken; private getSdkAuthToken; private storeUserAuthToken; private getUserAuthToken; private parseErrorResponse; private handleError; setApiKey(apiKey: string): void; getApiKey(): string; setAppId(appId: string): void; getAppId(): string; setProduction(production: boolean): void; isProductionMode(): boolean; getBaseURL(): string; setTimeout(timeout: number): void; getTimeout(): number; getMode(): 'custom' | 'preconfigured'; } /** * Contest API methods */ declare class ContestAPI { private client; constructor(client: LuqtaClient); /** Get all contests (Application API - no user required) */ getAll(params?: { page?: number; per_page?: number; }): Promise>; /** Get trending contests (Application API - no user required) */ getTrending(): Promise>; /** Get premium contests (Application API - no user required) */ getPremium(): Promise>; /** Get recent contests (Application API - no user required) */ getRecent(): Promise>; /** Participate in a contest (End-User API - requires initialization) */ participate(contestId: number, accessCode?: string): Promise>; /** Get contest progress (End-User API - requires initialization) */ getProgress(contestId: number): Promise>; /** Get contest history (End-User API - requires initialization) */ getHistory(): Promise>; /** Get contest compete data with levels progress (End-User API - requires initialization) */ compete(contestId: number): Promise>; } /** * Level API methods */ declare class LevelAPI { private client; constructor(client: LuqtaClient); /** Complete a level (End-User API) */ complete(levelId: number, data: { textContent?: string; qrCode?: string; link?: string; }): Promise>; /** Complete level with image (End-User API) */ completeWithImage(levelId: number, imageUrl: string): Promise>; /** * Complete a `client_webhook` level via the dedicated * `/sdk/app/client-webhook` endpoint (End-User API). * * `variables` is one entry per `Level.client_variables` item, each shaped * as `{ variable_name, data_type, value }`. */ completeClientWebhook(levelId: number, variables: Array<{ variable_name: string; data_type: string; value: unknown; }>): Promise>; /** Update level progress (End-User API) */ updateProgress(levelId: number): Promise>; /** Get congratulation screen data (End-User API) */ getCongratulation(levelId: number, contestId: number): Promise>; /** Scan QR code (End-User API) */ scanQR(qrData: string): Promise>; } /** * Quiz API methods */ declare class QuizAPI { private client; constructor(client: LuqtaClient); /** Start a quiz (End-User API) */ start(quizId: number, levelId: number): Promise>; /** Submit an answer (End-User API) */ submitAnswer(attemptId: number, questionId: number, optionId: number, levelId: number): Promise>; /** Submit/complete quiz (End-User API) */ submit(attemptId: number): Promise>; } /** * Reward API methods */ declare class RewardAPI { private client; constructor(client: LuqtaClient); /** Get rewards list (Application API - no user required) */ getList(): Promise>; /** Get user earnings (End-User API) */ getEarnings(): Promise>; /** Redeem a reward (End-User API) */ redeem(rewardId: number, points: number): Promise>; /** Get reward history (End-User API) */ getHistory(): Promise>; /** Get prize history (End-User API) */ getPrizeHistory(): Promise>; } /** * Notification API methods */ declare class NotificationAPI { private client; constructor(client: LuqtaClient); /** Get all notifications (End-User API) */ getAll(): Promise>; /** Mark notifications as read (End-User API) */ markAsRead(notificationIds: number[]): Promise>; /** Update notification settings (End-User API) */ updateSettings(settings: { push_enabled?: boolean; email_enabled?: boolean; }): Promise>; } /** * Profile API methods */ declare class ProfileAPI { private client; constructor(client: LuqtaClient); /** Get user profile (End-User API) */ get(): Promise>; /** Get user activities (End-User API) */ getActivities(): Promise>; /** Get user progress (End-User API) */ getProgress(): Promise>; /** Submit app feedback (End-User API) */ submitFeedback(rating: number, feedback: string): Promise>; /** Delete user account (End-User API) */ deleteAccount(): Promise>; } export {};