diff --git a/credentials/SecloreProtect.credentials.ts b/credentials/SecloreProtect.credentials.ts index e69de29..f2c60bc 100644 --- a/credentials/SecloreProtect.credentials.ts +++ b/credentials/SecloreProtect.credentials.ts @@ -0,0 +1,58 @@ +import { + ICredentialTestRequest, + ICredentialType, + INodeProperties, +} from 'n8n-workflow'; + +export class SecloreProtectApi implements ICredentialType { + name = 'secloreProtectApi'; + displayName = 'Seclore Protect API'; + documentationUrl = 'https://docs.seclore.com/'; + properties: INodeProperties[] = [ + { + displayName: 'Base URL', + name: 'baseUrl', + type: 'string', + default: 'https://api.seclore.com', + placeholder: 'https://api.seclore.com', + description: 'The base URL of your Seclore API instance', + required: true, + }, + { + displayName: 'Tenant ID', + name: 'tenantId', + type: 'string', + default: '', + placeholder: 'your-tenant-id', + description: 'Your Seclore tenant ID', + required: true, + }, + { + displayName: 'Tenant Secret', + name: 'tenantSecret', + type: 'string', + typeOptions: { + password: true, + }, + default: '', + description: 'Your Seclore tenant secret', + required: true, + }, + ]; + + // Optional: Add credential test + test: ICredentialTestRequest = { + request: { + baseURL: '={{$credentials.baseUrl}}', + url: '/seclore/drm/1.0/auth/login', + method: 'POST', + body: { + tenantId: '={{$credentials.tenantId}}', + tenantSecret: '={{$credentials.tenantSecret}}', + }, + headers: { + 'Content-Type': 'application/json', + }, + }, + }; +} diff --git a/nodes/SecloreProtect/SecloreProtect.node.json b/nodes/SecloreProtect/SecloreProtect.node.json index e69de29..b88e26f 100644 --- a/nodes/SecloreProtect/SecloreProtect.node.json +++ b/nodes/SecloreProtect/SecloreProtect.node.json @@ -0,0 +1,4 @@ +{ + "node": "dist/nodes/SecloreProtect/SecloreProtect.node.js", + "credentials": "dist/credentials/SecloreProtectApi.credentials.js" +} diff --git a/nodes/SecloreProtect/SecloreProtect.node.ts b/nodes/SecloreProtect/SecloreProtect.node.ts index 30713a9..accc028 100644 --- a/nodes/SecloreProtect/SecloreProtect.node.ts +++ b/nodes/SecloreProtect/SecloreProtect.node.ts @@ -1 +1,229 @@ -import { IDataObject, INodeExecutionData, IExecuteFunctions } from 'n8n-workflow'; +import { + IExecuteFunctions, + INodeExecutionData, + INodeType, + INodeTypeDescription, + NodeOperationError, +} from 'n8n-workflow'; + +import { SecloreDRMFileService } from './Services/SecloreDRMFileService'; + +export class SecloreProtect implements INodeType { + description: INodeTypeDescription = { + displayName: 'Seclore Protect', + name: 'secloreProtect', + icon: 'file:SecloreProtect.svg', + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"]}}', + description: 'Protect files using Seclore DRM with HotFolder configuration', + defaults: { + name: 'Seclore Protect', + }, + inputs: ['main'], + outputs: ['main'], + credentials: [ + { + name: 'secloreProtectApi', + required: true, + }, + ], + properties: [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Protect File with HotFolder', + value: 'protectWithHotFolder', + description: 'Protect a file using HotFolder ID configuration', + action: 'Protect file with HotFolder', + }, + ], + default: 'protectWithHotFolder', + }, + { + displayName: 'HotFolder ID', + name: 'hotfolderId', + type: 'string', + required: true, + default: '', + placeholder: 'e.g., hf-12345', + description: 'The ID of the HotFolder configuration to use for protection', + displayOptions: { + show: { + operation: ['protectWithHotFolder'], + }, + }, + }, + { + displayName: 'Input Binary Property', + name: 'binaryPropertyName', + type: 'string', + default: 'data', + required: true, + description: 'Name of the binary property that contains the file to protect', + displayOptions: { + show: { + operation: ['protectWithHotFolder'], + }, + }, + }, + { + displayName: 'Output Binary Property', + name: 'outputBinaryPropertyName', + type: 'string', + default: 'data', + required: true, + description: 'Name of the binary property where the protected file will be stored', + displayOptions: { + show: { + operation: ['protectWithHotFolder'], + }, + }, + }, + { + displayName: 'Correlation ID', + name: 'correlationId', + type: 'string', + default: '', + placeholder: 'e.g., req-12345', + description: 'Optional correlation ID for request tracking and logging', + displayOptions: { + show: { + operation: ['protectWithHotFolder'], + }, + }, + }, + { + displayName: 'Retry Count', + name: 'retryCount', + type: 'number', + default: 3, + description: 'Number of retry attempts for failed requests', + displayOptions: { + show: { + operation: ['protectWithHotFolder'], + }, + }, + }, + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + // Get credentials + const credentials = await this.getCredentials('secloreProtectApi'); + const baseUrl = credentials.baseUrl as string; + const tenantId = credentials.tenantId as string; + const tenantSecret = credentials.tenantSecret as string; + + // Initialize the file service + const fileService = new SecloreDRMFileService( + this, + baseUrl, + tenantId, + tenantSecret + ); + + // Get node parameters + const operation = this.getNodeParameter('operation', 0) as string; + + for (let i = 0; i < items.length; i++) { + try { + if (operation === 'protectWithHotFolder') { + // Get parameters for this item + const hotfolderId = this.getNodeParameter('hotfolderId', i) as string; + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string; + const outputBinaryPropertyName = this.getNodeParameter('outputBinaryPropertyName', i) as string; + const correlationId = this.getNodeParameter('correlationId', i) as string; + const retryCount = this.getNodeParameter('retryCount', i) as number; + + // Validate required parameters + if (!hotfolderId) { + throw new NodeOperationError(this.getNode(), 'HotFolder ID is required', { + itemIndex: i, + }); + } + + // Get input binary data + const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName); + const fileBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName); + + // Upload the file first + const uploadResult = await fileService.uploadFile( + new Uint8Array(fileBuffer), + binaryData.fileName || 'file', + correlationId || undefined, + retryCount + ); + + // Protect the uploaded file with HotFolder + const protectResult = await fileService.protectWithHotFolder( + { + hotfolderId, + fileStorageId: uploadResult.fileStorageId, + }, + correlationId || undefined, + retryCount + ); + + // Download the protected file + const protectedFileData = await fileService.downloadFile( + protectResult.fileStorageId, + correlationId || undefined, + retryCount + ); + + // Create output binary data + const outputBinaryData = await this.helpers.prepareBinaryData( + protectedFileData.buffer, + binaryData.fileName || 'protected_file', + binaryData.mimeType + ); + + // Create return item with binary data and metadata + const returnItem: INodeExecutionData = { + json: { + success: true, + originalFileStorageId: uploadResult.fileStorageId, + protectedFileStorageId: protectResult.fileStorageId, + secloreFileId: protectResult.secloreFileId, + hotfolderId, + fileName: binaryData.fileName, + fileSize: protectedFileData.length, + correlationId: correlationId || null, + }, + binary: { + [outputBinaryPropertyName]: outputBinaryData, + }, + }; + + returnData.push(returnItem); + } + } catch (error) { + // Handle errors gracefully + if (this.continueOnFail()) { + const returnItem: INodeExecutionData = { + json: { + success: false, + error: error.message, + itemIndex: i, + }, + }; + returnData.push(returnItem); + } else { + throw new NodeOperationError(this.getNode(), error.message, { + itemIndex: i, + }); + } + } + } + + return [returnData]; + } +} diff --git a/nodes/SecloreProtect/Services/SecloreDRMApiService.ts b/nodes/SecloreProtect/Services/SecloreDRMApiService.ts index 6322fff..9cfa37d 100644 --- a/nodes/SecloreProtect/Services/SecloreDRMApiService.ts +++ b/nodes/SecloreProtect/Services/SecloreDRMApiService.ts @@ -325,4 +325,43 @@ export class SecloreDRMApiService { } } + /** + * Downloads file with fileStorageId from file storage of currently logged in Tenant. + * NOTE: Files whose fileStorageId has 'DL_' prefix will be deleted from the file storage after download. + * + * @param fileStorageId - Storage ID of the file to be retrieved + * @param accessToken - JWT access token for authorization + * @param correlationId - Optional request ID for logging purpose + * @returns Promise - The downloaded file data + * @throws Error on authentication failure or server error + */ + async downloadFile( + fileStorageId: string, + accessToken: string, + correlationId?: string + ): Promise { + const headers: { [key: string]: string } = { + 'Authorization': `Bearer ${accessToken}`, + }; + + // Add correlation ID if provided + if (correlationId) { + headers['X-SECLORE-CORRELATION-ID'] = correlationId; + } + + const options: IHttpRequestOptions = { + method: 'GET', + url: `${this.baseUrl}/seclore/drm/filestorage/1.0/download/${fileStorageId}`, + headers, + encoding: 'arraybuffer', + }; + + try { + const response = await this.context.helpers.httpRequest(options); + return new Uint8Array(response as ArrayBuffer); + } catch (error: any) { + this.handleHttpError(error); + } + } + } diff --git a/nodes/SecloreProtect/Services/SecloreDRMFileService.ts b/nodes/SecloreProtect/Services/SecloreDRMFileService.ts index ae0b93d..ae078cc 100644 --- a/nodes/SecloreProtect/Services/SecloreDRMFileService.ts +++ b/nodes/SecloreProtect/Services/SecloreDRMFileService.ts @@ -265,6 +265,22 @@ export class SecloreDRMFileService { ); } + /** + * 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) */ @@ -279,10 +295,4 @@ export class SecloreDRMFileService { return !!this.accessToken && (!this.tokenExpiry || new Date() < this.tokenExpiry); } - /** - * Force logout (clears all tokens) - */ - logout(): void { - this.clearTokens(); - } }