diff --git a/nodes/SecloreProtect/Services/SecloreDRMFileService.ts b/nodes/SecloreProtect/Services/SecloreDRMFileService.ts new file mode 100644 index 0000000..ae0b93d --- /dev/null +++ b/nodes/SecloreProtect/Services/SecloreDRMFileService.ts @@ -0,0 +1,288 @@ +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 + ); + } + + /** + * 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); + } + + /** + * Force logout (clears all tokens) + */ + logout(): void { + this.clearTokens(); + } +}