recoder-shared
Version:
Shared types, utilities, and configurations for Recoder
406 lines • 13.7 kB
JavaScript
"use strict";
/**
* Unified Authentication Client for Recoder.xyz
* Supports all platforms: CLI, Web, Mobile, Desktop, Extension
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.authClient = exports.AuthClient = void 0;
const tslib_1 = require("tslib");
const axios_1 = tslib_1.__importDefault(require("axios"));
const events_1 = require("events");
class AuthClient extends events_1.EventEmitter {
constructor(baseURL = 'http://localhost:3001') {
super();
this.currentUser = null;
this.tokens = null;
this.deviceInfo = null;
this.refreshPromise = null;
this.baseURL = baseURL;
this.api = axios_1.default.create({
baseURL: `${baseURL}/api`,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
'User-Agent': this.getUserAgent()
}
});
this.setupInterceptors();
this.loadFromStorage();
}
getUserAgent() {
if (typeof window !== 'undefined' && window) {
return `Recoder-Web/${this.getVersion()}`;
}
else if (typeof process !== 'undefined' && process) {
return `Recoder-CLI/${this.getVersion()} (${process.platform})`;
}
return `Recoder-Client/${this.getVersion()}`;
}
getVersion() {
try {
// Try to get version from package.json
return '1.0.0'; // Fallback version
}
catch {
return '1.0.0';
}
}
setupInterceptors() {
// Request interceptor to add auth token
this.api.interceptors.request.use((config) => {
if (this.tokens?.accessToken) {
config.headers.Authorization = `Bearer ${this.tokens.accessToken}`;
}
return config;
});
// Response interceptor to handle token refresh
this.api.interceptors.response.use((response) => response, async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const refreshed = await this.refreshAccessToken();
if (refreshed) {
originalRequest.headers.Authorization = `Bearer ${this.tokens.accessToken}`;
return this.api(originalRequest);
}
else {
this.logout();
}
}
return Promise.reject(error);
});
}
// Authentication Methods
async register(email, password, name, organization) {
try {
const response = await this.api.post('/auth/register', {
email,
password,
name,
organization
});
await this.handleAuthSuccess(response.data);
return response.data;
}
catch (error) {
throw this.handleAuthError(error);
}
}
async login(email, password) {
try {
const response = await this.api.post('/auth/login', {
email,
password
});
await this.handleAuthSuccess(response.data);
return response.data;
}
catch (error) {
throw this.handleAuthError(error);
}
}
async loginWithGoogle(authCode, redirectUri) {
try {
const response = await this.api.post('/oauth/google', {
code: authCode,
redirectUri
});
await this.handleAuthSuccess(response.data);
return response.data;
}
catch (error) {
throw this.handleAuthError(error);
}
}
async loginWithGitHub(authCode, state) {
try {
const response = await this.api.post('/oauth/github', {
code: authCode,
state
});
await this.handleAuthSuccess(response.data);
return response.data;
}
catch (error) {
throw this.handleAuthError(error);
}
}
async refreshAccessToken() {
if (this.refreshPromise) {
return this.refreshPromise;
}
if (!this.tokens?.refreshToken) {
return false;
}
this.refreshPromise = this._performRefresh();
const result = await this.refreshPromise;
this.refreshPromise = null;
return result;
}
async _performRefresh() {
try {
const response = await this.api.post('/auth/refresh', {
refreshToken: this.tokens.refreshToken
});
this.tokens = {
accessToken: response.data.data.accessToken,
refreshToken: response.data.data.refreshToken
};
this.saveToStorage();
this.emit('tokenRefreshed', this.tokens);
return true;
}
catch (error) {
console.error('Token refresh failed:', error);
return false;
}
}
async logout() {
try {
if (this.tokens?.accessToken) {
await this.api.post('/auth/logout');
}
}
catch (error) {
console.error('Logout request failed:', error);
}
finally {
this.clearAuth();
this.emit('logout');
}
}
// Device Management
async registerDevice(deviceInfo) {
const deviceId = await this.generateDeviceId(deviceInfo.deviceType, deviceInfo.platform);
const fullDeviceInfo = {
...deviceInfo,
deviceId
};
try {
await this.api.post('/devices/register', fullDeviceInfo);
this.deviceInfo = fullDeviceInfo;
this.saveToStorage();
this.emit('deviceRegistered', fullDeviceInfo);
return fullDeviceInfo;
}
catch (error) {
throw this.handleAuthError(error);
}
}
async getDevices() {
try {
const response = await this.api.get('/devices');
return response.data.data;
}
catch (error) {
throw this.handleAuthError(error);
}
}
async sendHeartbeat(metadata) {
if (!this.deviceInfo)
return;
try {
await this.api.post(`/devices/${this.deviceInfo.deviceId}/heartbeat`, { metadata });
}
catch (error) {
console.error('Heartbeat failed:', error);
}
}
// OAuth Account Management
async getOAuthAccounts() {
try {
const response = await this.api.get('/oauth/accounts');
return response.data.data;
}
catch (error) {
throw this.handleAuthError(error);
}
}
async disconnectOAuthProvider(provider) {
try {
await this.api.delete(`/oauth/disconnect/${provider}`);
this.emit('oauthDisconnected', provider);
}
catch (error) {
throw this.handleAuthError(error);
}
}
// User Profile Management
async getCurrentUser() {
try {
const response = await this.api.get('/auth/me');
this.currentUser = response.data.data;
return response.data.data;
}
catch (error) {
throw this.handleAuthError(error);
}
}
async updateProfile(updates) {
try {
const response = await this.api.put('/auth/update-profile', updates);
this.currentUser = response.data.data;
this.emit('profileUpdated', response.data.data);
return response.data.data;
}
catch (error) {
throw this.handleAuthError(error);
}
}
async changePassword(currentPassword, newPassword) {
try {
await this.api.put('/auth/change-password', {
currentPassword,
newPassword
});
this.emit('passwordChanged');
}
catch (error) {
throw this.handleAuthError(error);
}
}
// Utility Methods
isAuthenticated() {
return !!(this.tokens?.accessToken && this.currentUser);
}
getUser() {
return this.currentUser;
}
getTokens() {
return this.tokens;
}
getDeviceInfo() {
return this.deviceInfo;
}
// Private Helper Methods
async handleAuthSuccess(response) {
this.currentUser = response.data.user;
this.tokens = {
accessToken: response.data.accessToken,
refreshToken: response.data.refreshToken
};
this.saveToStorage();
this.emit('authenticated', { user: this.currentUser, tokens: this.tokens });
// Auto-register device if not already registered
if (!this.deviceInfo && typeof window !== 'undefined' && window && typeof navigator !== 'undefined' && navigator) {
// Web platform
await this.registerDevice({
name: `${navigator.userAgent.includes('Chrome') ? 'Chrome' : 'Browser'} - ${new Date().toLocaleDateString()}`,
deviceType: 'web',
platform: 'browser'
});
}
}
handleAuthError(error) {
const message = error.response?.data?.error?.message || error.message || 'Authentication failed';
const authError = new Error(message);
this.emit('authError', authError);
return authError;
}
clearAuth() {
this.currentUser = null;
this.tokens = null;
this.removeFromStorage();
}
async generateDeviceId(deviceType, platform) {
try {
const response = await this.api.post('/devices/generate-id', {
deviceType,
platform
});
return response.data.data.deviceId;
}
catch (error) {
// Fallback to client-side generation
const timestamp = Date.now().toString(36);
const random = Math.random().toString(36).substr(2, 8);
return `${deviceType}-${platform}-${timestamp}-${random}`;
}
}
// Storage Methods (Platform-specific implementations should override these)
saveToStorage() {
const data = {
user: this.currentUser,
tokens: this.tokens,
device: this.deviceInfo
};
if (typeof window !== 'undefined' && window && window.localStorage && typeof localStorage !== 'undefined') {
// Web platform
localStorage.setItem('recoder-auth', JSON.stringify(data));
}
else if (typeof process !== 'undefined' && process) {
// Node.js platform (CLI)
const fs = require('fs');
const path = require('path');
const os = require('os');
try {
const configDir = path.join(os.homedir(), '.recoder');
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
const authFile = path.join(configDir, 'auth.json');
fs.writeFileSync(authFile, JSON.stringify(data, null, 2));
}
catch (error) {
console.error('Failed to save auth data:', error);
}
}
}
loadFromStorage() {
try {
let data = null;
if (typeof window !== 'undefined' && window && window.localStorage && typeof localStorage !== 'undefined') {
// Web platform
const stored = localStorage.getItem('recoder-auth');
if (stored) {
data = JSON.parse(stored);
}
}
else if (typeof process !== 'undefined' && process) {
// Node.js platform (CLI)
const fs = require('fs');
const path = require('path');
const os = require('os');
const authFile = path.join(os.homedir(), '.recoder', 'auth.json');
if (fs.existsSync(authFile)) {
const content = fs.readFileSync(authFile, 'utf-8');
data = JSON.parse(content);
}
}
if (data) {
this.currentUser = data.user;
this.tokens = data.tokens;
this.deviceInfo = data.device;
}
}
catch (error) {
console.error('Failed to load auth data:', error);
this.clearAuth();
}
}
removeFromStorage() {
if (typeof window !== 'undefined' && window && window.localStorage && typeof localStorage !== 'undefined') {
localStorage.removeItem('recoder-auth');
}
else if (typeof process !== 'undefined' && process) {
const fs = require('fs');
const path = require('path');
const os = require('os');
try {
const authFile = path.join(os.homedir(), '.recoder', 'auth.json');
if (fs.existsSync(authFile)) {
fs.unlinkSync(authFile);
}
}
catch (error) {
console.error('Failed to remove auth data:', error);
}
}
}
}
exports.AuthClient = AuthClient;
// Export singleton instance for convenience
exports.authClient = new AuthClient();
// Export for custom instances
exports.default = AuthClient;
//# sourceMappingURL=auth-client.js.map