@cabek/sdk
Version:
Official JavaScript/TypeScript SDK for C.A.B.E.K. biometric authentication
459 lines (454 loc) • 15.5 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var axios = require('axios');
var WebSocket = require('ws');
var EventEmitter = require('events');
/**
* C.A.B.E.K. JavaScript/TypeScript SDK
* Official SDK for integrating C.A.B.E.K. biometric authentication
*
* @packageDocumentation
*/
/**
* Authentication state enum
*/
exports.AuthenticationState = void 0;
(function (AuthenticationState) {
AuthenticationState["IDLE"] = "idle";
AuthenticationState["AUTHENTICATING"] = "authenticating";
AuthenticationState["AUTHENTICATED"] = "authenticated";
AuthenticationState["FAILED"] = "failed";
AuthenticationState["LOCKED"] = "locked";
})(exports.AuthenticationState || (exports.AuthenticationState = {}));
/**
* Device types supported by C.A.B.E.K.
*/
exports.DeviceType = void 0;
(function (DeviceType) {
DeviceType["SMARTWATCH"] = "smartwatch";
DeviceType["CHEST_STRAP"] = "chest_strap";
DeviceType["PATCH"] = "patch";
DeviceType["WEB"] = "web";
})(exports.DeviceType || (exports.DeviceType = {}));
/**
* Custom exceptions for C.A.B.E.K. SDK
*/
class CABEKException extends Error {
constructor(message) {
super(message);
this.name = 'CABEKException';
}
}
class AuthenticationException extends CABEKException {
constructor(message) {
super(message);
this.name = 'AuthenticationException';
}
}
class ConnectionException extends CABEKException {
constructor(message) {
super(message);
this.name = 'ConnectionException';
}
}
class SignalQualityException extends CABEKException {
constructor(message) {
super(message);
this.name = 'SignalQualityException';
}
}
/**
* Main C.A.B.E.K. SDK Client
*
* @example
* ```typescript
* const cabek = new CABEK({ apiKey: 'sk_live_...' });
* const result = await cabek.authenticate(ecgData);
* ```
*/
class CABEK extends EventEmitter {
/**
* Create a new C.A.B.E.K. client
* @param config - Configuration options
*/
constructor(config) {
super();
this.state = exports.AuthenticationState.IDLE;
this.connectedDevices = new Map();
this.config = {
apiKey: config.apiKey,
apiUrl: config.apiUrl || 'https://api.cabek.io',
timeout: config.timeout || 30000,
autoReconnect: config.autoReconnect !== false,
debug: config.debug || false
};
// Initialize axios client
this.axios = axios.create({
baseURL: this.config.apiUrl,
timeout: this.config.timeout,
headers: {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json',
'X-SDK-Version': '1.0.0',
'X-SDK-Language': 'JavaScript'
}
});
// Add response interceptor for error handling
this.axios.interceptors.response.use(response => response, this.handleAxiosError.bind(this));
}
/**
* Handle axios errors
*/
handleAxiosError(error) {
if (error.response) {
const status = error.response.status;
const data = error.response.data;
if (status === 401) {
throw new AuthenticationException('Invalid API key');
}
else if (status === 429) {
throw new CABEKException('Rate limit exceeded');
}
else if (status >= 400) {
throw new CABEKException(`API error: ${data?.error || 'Unknown'}`);
}
}
else if (error.request) {
throw new ConnectionException('Connection failed: No response from server');
}
throw new ConnectionException(`Connection failed: ${error.message}`);
}
/**
* Generate a unique device ID
*/
generateDeviceId() {
const timestamp = Date.now().toString(36);
const random = Math.random().toString(36).substring(2, 15);
return `${timestamp}${random}`.substring(0, 16);
}
/**
* Log debug messages
*/
debug(message, data) {
if (this.config.debug) {
console.log(`[CABEK SDK] ${message}`, data || '');
}
}
/**
* Enroll a user for biometric authentication
*
* @param userId - Unique user identifier
* @param ecgData - ECG signal data for enrollment
* @param deviceId - Optional device identifier
* @param deviceType - Type of device used
* @returns Enrollment result
*/
async enroll(userId, ecgData, deviceId, deviceType = exports.DeviceType.WEB) {
// Validate ECG data
if (!ecgData || ecgData.length < 256) {
throw new SignalQualityException('Insufficient ECG data for enrollment (minimum 256 samples)');
}
const enrollmentData = {
user_id: userId,
ecg_data: ecgData.slice(0, 2048), // Use up to 8 seconds at 256Hz
device_id: deviceId || this.generateDeviceId(),
device_type: deviceType,
timestamp_ns: Date.now() * 1000000
};
try {
this.debug('Enrolling user', { userId, deviceType });
const response = await this.axios.post('/users/enroll', enrollmentData);
const result = {
success: response.data.enrolled || false,
userId: response.data.user_id,
deviceId: enrollmentData.device_id,
message: response.data.message,
error: response.data.error
};
this.emit('enrollment', result);
return result;
}
catch (error) {
const result = {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
this.emit('enrollment', result);
return result;
}
}
/**
* Authenticate using ECG biometric data
*
* @param ecgData - ECG signal data
* @param userId - Optional user identifier
* @param deviceId - Optional device identifier
* @returns Authentication result
*/
async authenticate(ecgData, userId, deviceId) {
this.state = exports.AuthenticationState.AUTHENTICATING;
this.emit('stateChange', this.state);
// Validate ECG data
if (!ecgData || ecgData.length < 128) {
this.state = exports.AuthenticationState.FAILED;
this.emit('stateChange', this.state);
throw new SignalQualityException('Insufficient ECG data (minimum 128 samples)');
}
const authData = {
ecg_data: ecgData.slice(0, 512), // Use up to 2 seconds at 256Hz
user_id: userId,
device_id: deviceId || this.generateDeviceId(),
timestamp_ns: Date.now() * 1000000
};
try {
this.debug('Authenticating', { userId, deviceId });
const response = await this.axios.post('/auth/verify', authData);
const result = {
success: response.data.authenticated || false,
confidence: response.data.confidence || 0,
ephemeralToken: response.data.ephemeral_token,
expiresMs: response.data.expires_ms || 100,
userId: response.data.user_id,
deviceId: authData.device_id,
error: response.data.error,
timestamp: Date.now() * 1000000
};
if (result.success) {
this.state = exports.AuthenticationState.AUTHENTICATED;
this.emit('authenticated', result);
}
else {
this.state = exports.AuthenticationState.FAILED;
this.emit('authenticationFailed', result);
}
this.emit('stateChange', this.state);
return result;
}
catch (error) {
this.state = exports.AuthenticationState.FAILED;
this.emit('stateChange', this.state);
const result = {
success: false,
confidence: 0,
expiresMs: 0,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: Date.now() * 1000000
};
this.emit('authenticationFailed', result);
return result;
}
}
/**
* Start continuous authentication stream
*
* @param ecgStreamCallback - Async generator that yields ECG data
* @param userId - User identifier
* @param deviceId - Optional device identifier
* @param authFrequencyHz - Authentication frequency in Hz
*/
async startContinuousAuth(ecgStreamCallback, userId, deviceId, authFrequencyHz = 10.0) {
const finalDeviceId = deviceId || this.generateDeviceId();
// Create continuous authentication session
const sessionData = {
user_id: userId,
device_id: finalDeviceId,
auth_frequency_hz: authFrequencyHz
};
const response = await this.axios.post('/auth/continuous/start', sessionData);
this.currentSessionId = response.data.session_id;
// Connect WebSocket for real-time updates
const wsUrl = this.config.apiUrl
.replace('http', 'ws')
.replace('https', 'wss') +
`/ws/auth/${this.currentSessionId}?api_key=${this.config.apiKey}`;
this.websocket = new WebSocket(wsUrl);
this.websocket.on('open', () => {
this.debug('WebSocket connected');
this.emit('websocketConnected');
});
this.websocket.on('message', (data) => {
try {
const message = JSON.parse(data);
this.emit('websocketMessage', message);
}
catch (error) {
this.debug('Failed to parse WebSocket message', error);
}
});
this.websocket.on('error', (error) => {
this.debug('WebSocket error', error);
this.emit('websocketError', error);
});
// Start authentication loop
const authInterval = 1000 / authFrequencyHz;
try {
for await (const ecgData of ecgStreamCallback) {
// Authenticate
const result = await this.authenticate(ecgData, userId, finalDeviceId);
// Send to WebSocket if connected
if (this.websocket?.readyState === WebSocket.OPEN) {
this.websocket.send(JSON.stringify({
type: 'auth_result',
session_id: this.currentSessionId,
result: result
}));
}
// Check for lock condition
if (!result.success) {
this.state = exports.AuthenticationState.LOCKED;
this.emit('locked', result);
this.emit('stateChange', this.state);
break;
}
// Wait for next interval
await this.sleep(authInterval);
}
}
finally {
// End session
if (this.currentSessionId) {
await this.endContinuousAuth();
}
}
}
/**
* End continuous authentication session
*/
async endContinuousAuth() {
if (!this.currentSessionId) {
return;
}
try {
await this.axios.post(`/auth/continuous/end/${this.currentSessionId}`);
this.debug('Continuous auth session ended');
}
catch (error) {
this.debug('Failed to end continuous auth session', error);
}
// Close WebSocket
if (this.websocket) {
this.websocket.close();
this.websocket = undefined;
}
this.currentSessionId = undefined;
}
/**
* Validate ECG signal quality
*
* @param ecgData - ECG signal data to validate
* @returns Signal quality metrics
*/
async validateSignalQuality(ecgData) {
const response = await this.axios.post('/signals/validate', {
ecg_data: ecgData.slice(0, 512)
});
return response.data;
}
/**
* Get session statistics
*
* @param sessionId - Optional session ID (uses current if not provided)
* @returns Session statistics
*/
async getSessionStats(sessionId) {
const id = sessionId || this.currentSessionId;
if (!id) {
return null;
}
const response = await this.axios.get(`/auth/sessions/${id}/stats`);
return response.data;
}
/**
* Connect a biometric device
*
* @param deviceType - Type of device
* @param connectionParams - Device-specific connection parameters
* @returns Device information
*/
async connectDevice(deviceType, connectionParams) {
const deviceId = connectionParams.device_id || this.generateDeviceId();
const device = {
deviceId,
deviceType,
model: connectionParams.model,
firmwareVersion: connectionParams.firmware_version,
batteryLevel: connectionParams.battery_level,
samplingRate: connectionParams.sampling_rate || 256,
isConnected: true,
lastSeen: new Date()
};
this.connectedDevices.set(deviceId, device);
this.emit('deviceConnected', device);
return device;
}
/**
* Disconnect a biometric device
*
* @param deviceId - Device identifier
*/
async disconnectDevice(deviceId) {
const device = this.connectedDevices.get(deviceId);
if (device) {
device.isConnected = false;
this.connectedDevices.delete(deviceId);
this.emit('deviceDisconnected', device);
}
}
/**
* Get all connected devices
*/
getConnectedDevices() {
return Array.from(this.connectedDevices.values());
}
/**
* Get current authentication state
*/
getState() {
return this.state;
}
/**
* Check if client is authenticated
*/
isAuthenticated() {
return this.state === exports.AuthenticationState.AUTHENTICATED;
}
/**
* Helper function to sleep
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Cleanup resources
*/
async cleanup() {
await this.endContinuousAuth();
this.removeAllListeners();
this.connectedDevices.clear();
}
}
/**
* Quick authentication helper
*
* @param apiKey - C.A.B.E.K. API key
* @param ecgData - ECG signal data
* @param userId - Optional user identifier
* @returns Authentication result
*/
async function quickAuthenticate(apiKey, ecgData, userId) {
const client = new CABEK({ apiKey });
try {
return await client.authenticate(ecgData, userId);
}
finally {
await client.cleanup();
}
}
exports.AuthenticationException = AuthenticationException;
exports.CABEK = CABEK;
exports.CABEKException = CABEKException;
exports.ConnectionException = ConnectionException;
exports.SignalQualityException = SignalQualityException;
exports.default = CABEK;
exports.quickAuthenticate = quickAuthenticate;
//# sourceMappingURL=cabek-sdk.js.map