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
JavaScript
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 };