import { IExecuteFunctions } from 'n8n-workflow'; import { SecloreDRMApiService } from './SecloreDRMApiService'; import { IProtectWithExternalRefIdRequest, IProtectWithExternalRefIdResponse, IProtectWithFileIdRequest, IProtectWithFileIdResponse, IProtectWithHotFolderRequest, IProtectWithHotFolderResponse } from './Interfaces/ProtectInterfaces'; import { IUnprotectRequest, IUnprotectResponse } from './Interfaces/UnprotectInterfaces'; import { IFileUploadResponse } from './Interfaces/FileStorageInterfaces'; export class SecloreDRMFileService { private apiService: SecloreDRMApiService; private tenantId: string; private tenantSecret: string; private accessToken?: string; private refreshToken?: string; private tokenExpiry?: Date; private refreshPromise?: Promise; private loginPromise?: Promise; constructor( context: IExecuteFunctions, baseUrl: string, tenantId: string, tenantSecret: string, private defaultRetryCount: number = 3 ) { this.apiService = new SecloreDRMApiService(context, baseUrl); this.tenantId = tenantId; this.tenantSecret = tenantSecret; } /** * Ensures we have a valid access token, logging in if necessary * @param correlationId - Optional correlation ID for logging */ private async ensureAuthenticated(correlationId?: string): Promise { // If we don't have a token or it's expired, login if (!this.accessToken || (this.tokenExpiry && new Date() >= this.tokenExpiry)) { // If there's already a login in progress, wait for it if (this.loginPromise) { await this.loginPromise; return; } // Start login and store the promise this.loginPromise = this.login(correlationId); try { await this.loginPromise; } finally { this.loginPromise = undefined; } } } /** * Performs login and stores tokens * @param correlationId - Optional correlation ID for logging */ private async login(correlationId?: string): Promise { try { const loginResponse = await this.apiService.login( this.tenantId, this.tenantSecret, correlationId ); this.accessToken = loginResponse.accessToken; this.refreshToken = loginResponse.refreshToken; // Set token expiry to 50 minutes from now (assuming 1 hour token life) this.tokenExpiry = new Date(Date.now() + 50 * 60 * 1000); } catch (error) { this.clearTokens(); throw error; } } /** * Attempts to refresh the access token with concurrency protection * @param correlationId - Optional correlation ID for logging */ private async refreshAccessToken(correlationId?: string): Promise { // If there's already a refresh in progress, wait for it if (this.refreshPromise) { await this.refreshPromise; return; } if (!this.refreshToken) { throw new Error('No refresh token available'); } // Start refresh and store the promise this.refreshPromise = this.performTokenRefresh(correlationId); try { await this.refreshPromise; } finally { this.refreshPromise = undefined; } } /** * Performs the actual token refresh * @param correlationId - Optional correlation ID for logging */ private async performTokenRefresh(correlationId?: string): Promise { try { const refreshResponse = await this.apiService.refreshToken( this.refreshToken!, correlationId ); this.accessToken = refreshResponse.accessToken; this.refreshToken = refreshResponse.refreshToken; // Set token expiry to 50 minutes from now this.tokenExpiry = new Date(Date.now() + 50 * 60 * 1000); } catch (error) { this.clearTokens(); throw error; } } /** * Clears stored tokens and pending promises */ private clearTokens(): void { this.accessToken = undefined; this.refreshToken = undefined; this.tokenExpiry = undefined; this.refreshPromise = undefined; this.loginPromise = undefined; } /** * Executes an API call with automatic authentication and retry logic * @param apiCall - The API call function to execute * @param retryCount - Number of retries (defaults to class default) * @param correlationId - Optional correlation ID for logging */ private async executeWithRetry( apiCall: (accessToken: string) => Promise, retryCount: number = this.defaultRetryCount, correlationId?: string ): Promise { let lastError: Error; for (let attempt = 0; attempt <= retryCount; attempt++) { try { // Ensure we have a valid token await this.ensureAuthenticated(correlationId); if (!this.accessToken) { throw new Error('Failed to obtain access token'); } // Execute the API call return await apiCall(this.accessToken); } catch (error: any) { lastError = error; // If it's an authentication error and we have retries left, try to refresh token if (error.message.includes('Unauthorized') && attempt < retryCount) { try { await this.refreshAccessToken(correlationId); continue; // Retry with new token } catch (refreshError) { // If refresh fails, clear tokens and try full login on next attempt this.clearTokens(); } } // If it's the last attempt or not an auth error, throw if (attempt === retryCount) { throw lastError; } // Wait before retry (exponential backoff) await new Promise(resolve => { const delay = Math.pow(2, attempt) * 1000; // Use a simple delay implementation const start = Date.now(); while (Date.now() - start < delay) { // Busy wait for delay } resolve(undefined); }); } } throw lastError!; } /** * Protect file using external identifier with automatic authentication and retry */ async protectWithExternalRefId( protectRequest: IProtectWithExternalRefIdRequest, correlationId?: string, retryCount?: number ): Promise { return this.executeWithRetry( (accessToken) => this.apiService.protectWithExternalRefId(protectRequest, accessToken, correlationId), retryCount, correlationId ); } /** * Protect file using existing protected file ID with automatic authentication and retry */ async protectWithFileId( protectRequest: IProtectWithFileIdRequest, correlationId?: string, retryCount?: number ): Promise { return this.executeWithRetry( (accessToken) => this.apiService.protectWithFileId(protectRequest, accessToken, correlationId), retryCount, correlationId ); } /** * Protect file using HotFolder ID with automatic authentication and retry */ async protectWithHotFolder( protectRequest: IProtectWithHotFolderRequest, correlationId?: string, retryCount?: number ): Promise { return this.executeWithRetry( (accessToken) => this.apiService.protectWithHotFolder(protectRequest, accessToken, correlationId), retryCount, correlationId ); } /** * Unprotect file with automatic authentication and retry */ async unprotect( unprotectRequest: IUnprotectRequest, correlationId?: string, retryCount?: number ): Promise { return this.executeWithRetry( (accessToken) => this.apiService.unprotect(unprotectRequest, accessToken, correlationId), retryCount, correlationId ); } /** * Upload file with automatic authentication and retry */ async uploadFile( fileBuffer: Uint8Array, fileName: string, correlationId?: string, retryCount?: number ): Promise { return this.executeWithRetry( (accessToken) => this.apiService.uploadFile(fileBuffer, fileName, accessToken, correlationId), retryCount, correlationId ); } /** * Download file with automatic authentication and retry * NOTE: Files whose fileStorageId has 'DL_' prefix will be deleted from the file storage after download. */ async downloadFile( fileStorageId: string, correlationId?: string, retryCount?: number ): Promise { return this.executeWithRetry( (accessToken) => this.apiService.downloadFile(fileStorageId, accessToken, correlationId), retryCount, correlationId ); } /** * Get current access token (for debugging/monitoring) */ getAccessToken(): string | undefined { return this.accessToken; } /** * Check if currently authenticated */ isAuthenticated(): boolean { return !!this.accessToken && (!this.tokenExpiry || new Date() < this.tokenExpiry); } }