UNPKG

rauth-provider

Version:

A lightweight, plug-and-play Node.js library for phone number authentication using the Rauth.io reverse verification flow via WhatsApp or SMS.

300 lines (258 loc) 8.89 kB
import { SessionStore } from './sessionStore.js'; import { RevokedStore } from './revokedStore.js'; import { Utils } from './utils.js'; import { RauthApiClient } from './apiClient.js'; /** * Main RauthProvider class for managing authentication sessions */ class RauthProvider { constructor() { this.sessionStore = new SessionStore(); this.revokedStore = new RevokedStore(); this.config = { rauth_api_key: null, app_id: null, webhook_secret: null, // webhook_url removed default_session_ttl: 900, // 15 minutes default_revoked_ttl: 3600, // 1 hour }; this.apiClient = null; this.initialized = false; } /** * Initialize the RauthProvider with configuration * @param {Object} options - Configuration options * @param {string} options.rauth_api_key - Rauth API key * @param {string} options.app_id - Application ID * @param {string} options.webhook_secret - Webhook secret for verification * @param {number} [options.default_session_ttl=900] - Default session TTL in seconds * @param {number} [options.default_revoked_ttl=3600] - Default revoked session TTL in seconds */ init(options = {}) { const required = ['rauth_api_key', 'app_id', 'webhook_secret']; const validation = Utils.validatePayload(options, required); if (!validation.success) { throw new Error(`RauthProvider.init(): Missing required fields: ${validation.missing.join(', ')}`); } // Remove webhook_url from config merging this.config = { ...this.config, ...options, }; // Initialize API client this.apiClient = new RauthApiClient(this.config); this.initialized = true; } /** * Verify a session with phone number * @param {string} sessionToken - Session token * @param {string} userPhone - User's phone number * @returns {Promise<boolean>} True if session is valid */ async verifySession(sessionToken, userPhone) { this._ensureInitialized(); // Check if session is revoked locally first if (this.revokedStore.isSessionRevoked(sessionToken)) { return false; } // Check local memory first const localSession = this.sessionStore.getSession(sessionToken); if (localSession && localSession.phone === userPhone) { return true; } // If not found locally, check with API try { const apiResponse = await this.apiClient.verifySession(sessionToken); if (!apiResponse) { return false; // Session not found } // If session is revoked, add to revoked store if (apiResponse.revoked) { this.revokedStore.revokeSession( sessionToken, apiResponse.ttl || this.config.default_revoked_ttl, apiResponse.reason || 'revoked' ); return false; } // If session is verified, add to session store if (apiResponse.verified && apiResponse.phone === userPhone) { this.sessionStore.createSession( apiResponse.phone, sessionToken, apiResponse.ttl || this.config.default_session_ttl ); return true; } return false; } catch (error) { // If API call fails, rely on local memory only console.warn(`Failed to verify session with API: ${error.message}`); return false; } } /** * Check if a session is revoked * @param {string} sessionToken - Session token * @returns {Promise<boolean>} True if session is revoked */ async isSessionRevoked(sessionToken) { this._ensureInitialized(); // Check local memory first if (this.revokedStore.isSessionRevoked(sessionToken)) { return true; } // If not found locally, check with API try { const apiResponse = await this.apiClient.verifySession(sessionToken); if (!apiResponse) { return false; // Session not found } // If session is revoked, add to revoked store if (apiResponse.revoked) { this.revokedStore.revokeSession( sessionToken, apiResponse.ttl || this.config.default_revoked_ttl, apiResponse.reason || 'revoked' ); return true; } return false; } catch (error) { // If API call fails, rely on local memory only console.warn(`Failed to check revocation with API: ${error.message}`); return false; } } /** * Create a session (used by webhooks) * @param {string} phone - Phone number * @param {string} sessionToken - Session token * @param {number} ttl - Time to live in seconds */ createSession(phone, sessionToken, ttl) { this._ensureInitialized(); this.sessionStore.createSession(phone, sessionToken, ttl); } /** * Revoke a session (used by webhooks) * @param {string} sessionToken - Session token * @param {number} ttl - Time to live in seconds * @param {string} reason - Revocation reason */ revokeSession(sessionToken, ttl, reason = 'revoked') { this._ensureInitialized(); // Remove from active sessions this.sessionStore.removeSession(sessionToken); // Add to revoked sessions this.revokedStore.revokeSession(sessionToken, ttl, reason); } /** * Get webhook handler for Express routes * @returns {Function} Express middleware function */ webhookHandler() { this._ensureInitialized(); return (req, res, next) => { try { // Extract webhook secret from headers const webhookSecret = req.headers['x-webhook-secret']; if (!webhookSecret || webhookSecret !== this.config.webhook_secret) { return res.status(401).json({ success: false, error: 'Unauthorized: Invalid webhook secret' }); } const payload = req.body; if (!payload || !payload.event) { return res.status(400).json({ success: false, error: 'Invalid payload: Missing event field' }); } // Handle different webhook events switch (payload.event) { case 'session_created': this._handleSessionCreated(payload); break; case 'session_revoked': this._handleSessionRevoked(payload); break; default: return res.status(400).json({ success: false, error: `Unknown event: ${payload.event}` }); } res.status(200).json({ success: true, message: 'Webhook processed successfully' }); } catch (error) { console.error('Webhook handler error:', error); res.status(500).json({ success: false, error: 'Internal server error' }); } }; } /** * Handle session_created webhook event * @param {Object} payload - Webhook payload * @private */ _handleSessionCreated(payload) { const validation = Utils.validatePayload(payload, ['session_token', 'phone', 'ttl']); if (!validation.success) { throw new Error(`Invalid session_created payload: Missing ${validation.missing.join(', ')}`); } this.createSession(payload.phone, payload.session_token, payload.ttl); } /** * Handle session_revoked webhook event * @param {Object} payload - Webhook payload * @private */ _handleSessionRevoked(payload) { const validation = Utils.validatePayload(payload, ['session_token', 'ttl']); if (!validation.success) { throw new Error(`Invalid session_revoked payload: Missing ${validation.missing.join(', ')}`); } this.revokeSession(payload.session_token, payload.ttl, payload.reason); } /** * Check API health status * @returns {Promise<boolean>} True if API is healthy */ async checkApiHealth() { this._ensureInitialized(); try { return await this.apiClient.healthCheck(); } catch (error) { console.warn(`API health check failed: ${error.message}`); return false; } } /** * Clear all sessions (useful for testing) */ clearAllSessions() { this._ensureInitialized(); this.sessionStore.clear(); this.revokedStore.clear(); } /** * Ensure the provider is initialized * @private */ _ensureInitialized() { if (!this.initialized) { throw new Error('RauthProvider not initialized. Call init() first.'); } } } // Create and export singleton instance const rauthProvider = new RauthProvider(); export { rauthProvider as RauthProvider };