From 6e7e28a2e60dcd6c608a530fccc0257e3189205b Mon Sep 17 00:00:00 2001 From: "atharva.dev" Date: Fri, 24 Oct 2025 06:10:55 +0000 Subject: [PATCH] POC successful for protect with hf --- .gitignore | 2 +- credentials/SecloreProtect.credentials.ts | 58 -- credentials/SecloreProtectApi.credentials.ts | 58 ++ .../SecloreProtect.dark.svg | 0 icons/SecloreProtect.light.svg | 11 + icons/github.dark.svg | 3 - icons/github.svg | 3 - nodes/SecloreProtect/SecloreProtect.node.ts | 19 +- .../Services/SecloreDRMApiService.ts | 668 +++++++++--------- .../Services/SecloreDRMFileService.ts | 549 +++++++------- 10 files changed, 708 insertions(+), 663 deletions(-) delete mode 100644 credentials/SecloreProtect.credentials.ts create mode 100644 credentials/SecloreProtectApi.credentials.ts rename nodes/SecloreProtect/SecloreProtect.svg => icons/SecloreProtect.dark.svg (100%) create mode 100644 icons/SecloreProtect.light.svg delete mode 100644 icons/github.dark.svg delete mode 100644 icons/github.svg diff --git a/.gitignore b/.gitignore index 2a61503..b64847f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ dist node_modules package-lock.json -.n8n/.env \ No newline at end of file +.n8n \ No newline at end of file diff --git a/credentials/SecloreProtect.credentials.ts b/credentials/SecloreProtect.credentials.ts deleted file mode 100644 index f2c60bc..0000000 --- a/credentials/SecloreProtect.credentials.ts +++ /dev/null @@ -1,58 +0,0 @@ -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/credentials/SecloreProtectApi.credentials.ts b/credentials/SecloreProtectApi.credentials.ts new file mode 100644 index 0000000..f034503 --- /dev/null +++ b/credentials/SecloreProtectApi.credentials.ts @@ -0,0 +1,58 @@ +import { ICredentialTestRequest, ICredentialType, INodeProperties, Icon } from 'n8n-workflow'; + +export class SecloreProtectApi implements ICredentialType { + name = 'secloreProtectApi'; + displayName = 'Seclore Protect API'; + documentationUrl = 'https://docs.seclore.com/'; + icon: Icon = { + light: 'file:../icons/SecloreProtect.light.svg', + dark: 'file:../icons/SecloreProtect.dark.svg', + }; + 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.svg b/icons/SecloreProtect.dark.svg similarity index 100% rename from nodes/SecloreProtect/SecloreProtect.svg rename to icons/SecloreProtect.dark.svg diff --git a/icons/SecloreProtect.light.svg b/icons/SecloreProtect.light.svg new file mode 100644 index 0000000..58541ba --- /dev/null +++ b/icons/SecloreProtect.light.svg @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/icons/github.dark.svg b/icons/github.dark.svg deleted file mode 100644 index 0366b08..0000000 --- a/icons/github.dark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/icons/github.svg b/icons/github.svg deleted file mode 100644 index fe1ac05..0000000 --- a/icons/github.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/nodes/SecloreProtect/SecloreProtect.node.ts b/nodes/SecloreProtect/SecloreProtect.node.ts index 5ea9301..e51750b 100644 --- a/nodes/SecloreProtect/SecloreProtect.node.ts +++ b/nodes/SecloreProtect/SecloreProtect.node.ts @@ -12,7 +12,8 @@ export class SecloreProtect implements INodeType { description: INodeTypeDescription = { displayName: 'Seclore Protect', name: 'secloreProtect', - icon: 'file:SecloreProtect.svg', + icon: 'file:../../icons/SecloreProtect.light.svg', + usableAsTool: true, // TODO: make it false/ don't allow it to be used as a tool group: ['transform'], version: 1, subtitle: '={{$parameter["operation"]}}', @@ -39,7 +40,7 @@ export class SecloreProtect implements INodeType { name: 'Protect File with HotFolder', value: 'protectWithHotFolder', description: 'Protect a file using HotFolder ID configuration', - action: 'Protect file with HotFolder', + action: 'Protect file with hotfolder', }, ], default: 'protectWithHotFolder', @@ -129,6 +130,8 @@ export class SecloreProtect implements INodeType { const operation = this.getNodeParameter('operation', 0) as string; for (let i = 0; i < items.length; i++) { + console.log('Data'); + console.log(items[i]); try { if (operation === 'protectWithHotFolder') { // Get parameters for this item @@ -148,10 +151,16 @@ export class SecloreProtect implements INodeType { }); } + console.log('assertBinaryData'); // Get input binary data const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName); + + console.log('getBinaryDataBuffer'); const fileBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName); + console.log('fileBuffer', fileBuffer); + console.log('binaryData.fileName', binaryData.fileName); + // Upload the file first const uploadResult = await fileService.uploadFile( new Uint8Array(fileBuffer), @@ -160,6 +169,8 @@ export class SecloreProtect implements INodeType { retryCount, ); + console.log('File upload response', uploadResult); + // Protect the uploaded file with HotFolder const protectResult = await fileService.protectWithHotFolder( { @@ -170,6 +181,8 @@ export class SecloreProtect implements INodeType { retryCount, ); + console.log('File protect response', protectResult); + // Download the protected file const protectedFileData = await fileService.downloadFile( protectResult.fileStorageId, @@ -177,6 +190,8 @@ export class SecloreProtect implements INodeType { retryCount, ); + console.log('Protected file data', protectedFileData); + // Create output binary data const outputBinaryData = await this.helpers.prepareBinaryData( Buffer.from(protectedFileData), diff --git a/nodes/SecloreProtect/Services/SecloreDRMApiService.ts b/nodes/SecloreProtect/Services/SecloreDRMApiService.ts index 9cfa37d..f8713d7 100644 --- a/nodes/SecloreProtect/Services/SecloreDRMApiService.ts +++ b/nodes/SecloreProtect/Services/SecloreDRMApiService.ts @@ -1,367 +1,387 @@ -import { IExecuteFunctions, IHttpRequestOptions } from 'n8n-workflow'; -import { ILoginRequest, ILoginResponse, IRefreshTokenRequest } from './Interfaces/LoginInterfaces'; +import { IExecuteFunctions, IHttpRequestOptions, NodeApiError } from 'n8n-workflow'; import { IErrorResponse } from './Interfaces/ErrorInterfaces'; -import { IProtectWithExternalRefIdRequest, IProtectWithExternalRefIdResponse, IProtectWithFileIdRequest, IProtectWithFileIdResponse, IProtectWithHotFolderRequest, IProtectWithHotFolderResponse } from './Interfaces/ProtectInterfaces'; -import FormData from 'form-data'; -import { IUnprotectRequest, IUnprotectResponse } from './Interfaces/UnprotectInterfaces'; import { IFileUploadResponse } from './Interfaces/FileStorageInterfaces'; +import { ILoginRequest, ILoginResponse, IRefreshTokenRequest } from './Interfaces/LoginInterfaces'; +import { + IProtectWithExternalRefIdRequest, + IProtectWithExternalRefIdResponse, + IProtectWithFileIdRequest, + IProtectWithFileIdResponse, + IProtectWithHotFolderRequest, + IProtectWithHotFolderResponse, +} from './Interfaces/ProtectInterfaces'; +import { IUnprotectRequest, IUnprotectResponse } from './Interfaces/UnprotectInterfaces'; export class SecloreDRMApiService { - private baseUrl: string; + private baseUrl: string; - constructor(private context: IExecuteFunctions, baseUrl: string) { - this.baseUrl = baseUrl; - } + constructor( + private context: IExecuteFunctions, + baseUrl: string, + ) { + this.baseUrl = baseUrl; + } - /** - * Common error handler for HTTP responses - * @param error - The error object from httpRequest - * @param customMessages - Optional custom error messages for specific status codes - */ - private handleHttpError(error: any, customMessages?: { [statusCode: number]: string }): never { - const statusCode = error.statusCode; - const errorResponse = error.response?.body as IErrorResponse; - - if (customMessages && customMessages[statusCode]) { - throw new Error(`${customMessages[statusCode]}: ${errorResponse?.errorMessage || 'Unknown error'}`); - } - - // Default error handling - switch (statusCode) { - case 400: - throw new Error(`Bad Request: ${errorResponse?.errorMessage || 'Invalid request data'}`); - case 401: - throw new Error(`Unauthorized: ${errorResponse?.errorMessage || 'Invalid credentials'}`); - case 413: - throw new Error(`Payload Too Large: ${errorResponse?.errorMessage || 'File size exceeds limit'}`); - case 500: - throw new Error(`Server Error: ${errorResponse?.errorMessage || 'Internal server error'}`); - default: - throw error; - } - } + /** + * Common error handler for HTTP responses + * @param error - The error object from httpRequest + * @param customMessages - Optional custom error messages for specific status codes + */ + private handleHttpError( + error: NodeApiError, + customMessages?: { [statusCode: number]: string }, + ): never { + const statusCode: number = parseInt(error.httpCode ?? '0'); + const errorResponse = error.errorResponse as unknown as IErrorResponse; - /** - * Login Endpoint to generate Access Token and Refresh Token for JWT Authorization. - * Upon successful login, all the existing previous tokens for that tenant will be invalidated. - * - * @param tenantId - The tenant ID - * @param tenantSecret - The tenant secret - * @param correlationId - Optional request ID for logging purpose - * @returns Promise - Access token and refresh token - * @throws Error on authentication failure or server error - */ - async login(tenantId: string, tenantSecret: string, correlationId?: string): Promise { - const requestBody: ILoginRequest = { - tenantId, - tenantSecret, - }; + if (customMessages && customMessages[statusCode]) { + throw new Error( + `${customMessages[statusCode]}: ${errorResponse?.errorMessage || 'Unknown error'}`, + ); + } - const headers: { [key: string]: string } = { - 'Content-Type': 'application/json', - }; + // Default error handling + switch (statusCode) { + case 400: + throw new Error(`Bad Request: ${errorResponse?.errorMessage || 'Invalid request data'}`); + case 401: + throw new Error(`Unauthorized: ${errorResponse?.errorMessage || 'Invalid credentials'}`); + case 413: + throw new Error( + `Payload Too Large: ${errorResponse?.errorMessage || 'File size exceeds limit'}`, + ); + case 500: + throw new Error(`Server Error: ${errorResponse?.errorMessage || 'Internal server error'}`); + default: + throw error; + } + } - // Add correlation ID if provided - if (correlationId) { - headers['X-SECLORE-CORRELATION-ID'] = correlationId; - } + /** + * Login Endpoint to generate Access Token and Refresh Token for JWT Authorization. + * Upon successful login, all the existing previous tokens for that tenant will be invalidated. + * + * @param tenantId - The tenant ID + * @param tenantSecret - The tenant secret + * @param correlationId - Optional request ID for logging purpose + * @returns Promise - Access token and refresh token + * @throws Error on authentication failure or server error + */ + async login( + tenantId: string, + tenantSecret: string, + correlationId?: string, + ): Promise { + const requestBody: ILoginRequest = { + tenantId, + tenantSecret, + }; - const options: IHttpRequestOptions = { - method: 'POST', - url: `${this.baseUrl}/seclore/drm/1.0/auth/login`, - headers, - body: requestBody, - json: true, - }; + const headers: { [key: string]: string } = { + 'Content-Type': 'application/json', + }; - try { - const response = await this.context.helpers.httpRequest(options); - return response as ILoginResponse; - } catch (error: any) { - this.handleHttpError(error); - } - } + // Add correlation ID if provided + if (correlationId) { + headers['X-SECLORE-CORRELATION-ID'] = correlationId; + } - /** - * Endpoint for generating new Access Token and Refresh Token using an existing valid Refresh Token. - * Upon successful response, all the previous existing Access Tokens and Refresh Tokens of that tenant will be invalidated. - * - * @param refreshToken - The existing valid refresh token - * @param correlationId - Optional request ID for logging purpose - * @returns Promise - New access token and refresh token - * @throws Error on authentication failure or server error - */ - async refreshToken(refreshToken: string, correlationId?: string): Promise { - const requestBody: IRefreshTokenRequest = { - refreshToken, - }; + const options: IHttpRequestOptions = { + method: 'POST', + url: `${this.baseUrl}/seclore/drm/1.0/auth/login`, + headers, + body: requestBody, + json: true, + }; - const headers: { [key: string]: string } = { - 'Content-Type': 'application/json', - }; + try { + const response = await this.context.helpers.httpRequest(options); + return response as ILoginResponse; + } catch (error: unknown) { + this.handleHttpError(error as NodeApiError); + } + } - // Add correlation ID if provided - if (correlationId) { - headers['X-SECLORE-CORRELATION-ID'] = correlationId; - } + /** + * Endpoint for generating new Access Token and Refresh Token using an existing valid Refresh Token. + * Upon successful response, all the previous existing Access Tokens and Refresh Tokens of that tenant will be invalidated. + * + * @param refreshToken - The existing valid refresh token + * @param correlationId - Optional request ID for logging purpose + * @returns Promise - New access token and refresh token + * @throws Error on authentication failure or server error + */ + async refreshToken(refreshToken: string, correlationId?: string): Promise { + const requestBody: IRefreshTokenRequest = { + refreshToken, + }; - const options: IHttpRequestOptions = { - method: 'POST', - url: `${this.baseUrl}/seclore/drm/1.0/auth/refresh`, - headers, - body: requestBody, - json: true, - }; + const headers: { [key: string]: string } = { + 'Content-Type': 'application/json', + }; - try { - const response = await this.context.helpers.httpRequest(options); - return response as ILoginResponse; - } catch (error: any) { - this.handleHttpError(error, { 401: 'Unauthorized' }); - } - } + // Add correlation ID if provided + if (correlationId) { + headers['X-SECLORE-CORRELATION-ID'] = correlationId; + } - /** - * Protect file using external identifier of protected File and HotFolder with PS configured against the logged in Tenant in application. - * - * @param protectRequest - The protection request details - * @param accessToken - JWT access token for authorization - * @param correlationId - Optional request ID for logging purpose - * @returns Promise - File storage ID and Seclore file ID - * @throws Error on bad request, authentication failure or server error - */ - async protectWithExternalRefId( - protectRequest: IProtectWithExternalRefIdRequest, - accessToken: string, - correlationId?: string - ): Promise { - const headers: { [key: string]: string } = { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }; + const options: IHttpRequestOptions = { + method: 'POST', + url: `${this.baseUrl}/seclore/drm/1.0/auth/refresh`, + headers, + body: requestBody, + json: true, + }; - // Add correlation ID if provided - if (correlationId) { - headers['X-SECLORE-CORRELATION-ID'] = correlationId; - } + try { + const response = await this.context.helpers.httpRequest(options); + return response as ILoginResponse; + } catch (error: unknown) { + this.handleHttpError(error as NodeApiError, { 401: 'Unauthorized' }); + } + } - const options: IHttpRequestOptions = { - method: 'POST', - url: `${this.baseUrl}/seclore/drm/1.0/protect/externalref`, - headers, - body: protectRequest, - json: true, - }; + /** + * Protect file using external identifier of protected File and HotFolder with PS configured against the logged in Tenant in application. + * + * @param protectRequest - The protection request details + * @param accessToken - JWT access token for authorization + * @param correlationId - Optional request ID for logging purpose + * @returns Promise - File storage ID and Seclore file ID + * @throws Error on bad request, authentication failure or server error + */ + async protectWithExternalRefId( + protectRequest: IProtectWithExternalRefIdRequest, + accessToken: string, + correlationId?: string, + ): Promise { + const headers: { [key: string]: string } = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }; - try { - const response = await this.context.helpers.httpRequest(options); - return response as IProtectWithExternalRefIdResponse; - } catch (error: any) { - this.handleHttpError(error); - } - } + // Add correlation ID if provided + if (correlationId) { + headers['X-SECLORE-CORRELATION-ID'] = correlationId; + } - /** - * Protects file using File ID of already protected file with PS configured against the logged in Tenant in application. - * - * @param protectRequest - The protection request details with existing protected file ID - * @param accessToken - JWT access token for authorization - * @param correlationId - Optional request ID for logging purpose - * @returns Promise - File storage ID and Seclore file ID - * @throws Error on bad request, authentication failure or server error - */ - async protectWithFileId( - protectRequest: IProtectWithFileIdRequest, - accessToken: string, - correlationId?: string - ): Promise { - const headers: { [key: string]: string } = { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }; + const options: IHttpRequestOptions = { + method: 'POST', + url: `${this.baseUrl}/seclore/drm/1.0/protect/externalref`, + headers, + body: protectRequest, + json: true, + }; - // Add correlation ID if provided - if (correlationId) { - headers['X-SECLORE-CORRELATION-ID'] = correlationId; - } + try { + const response = await this.context.helpers.httpRequest(options); + return response as IProtectWithExternalRefIdResponse; + } catch (error: unknown) { + this.handleHttpError(error as NodeApiError); + } + } - const options: IHttpRequestOptions = { - method: 'POST', - url: `${this.baseUrl}/seclore/drm/1.0/protect/fileid`, - headers, - body: protectRequest, - json: true, - }; + /** + * Protects file using File ID of already protected file with PS configured against the logged in Tenant in application. + * + * @param protectRequest - The protection request details with existing protected file ID + * @param accessToken - JWT access token for authorization + * @param correlationId - Optional request ID for logging purpose + * @returns Promise - File storage ID and Seclore file ID + * @throws Error on bad request, authentication failure or server error + */ + async protectWithFileId( + protectRequest: IProtectWithFileIdRequest, + accessToken: string, + correlationId?: string, + ): Promise { + const headers: { [key: string]: string } = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }; - try { - const response = await this.context.helpers.httpRequest(options); - return response as IProtectWithFileIdResponse; - } catch (error: any) { - this.handleHttpError(error); - } - } + // Add correlation ID if provided + if (correlationId) { + headers['X-SECLORE-CORRELATION-ID'] = correlationId; + } - /** - * Protects file using HotFolder ID with PS configured against the logged in Tenant in application. - * - * @param protectRequest - The protection request details with hotfolder ID - * @param accessToken - JWT access token for authorization - * @param correlationId - Optional request ID for logging purpose - * @returns Promise - File storage ID and Seclore file ID - * @throws Error on bad request, authentication failure or server error - */ - async protectWithHotFolder( - protectRequest: IProtectWithHotFolderRequest, - accessToken: string, - correlationId?: string - ): Promise { - const headers: { [key: string]: string } = { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }; + const options: IHttpRequestOptions = { + method: 'POST', + url: `${this.baseUrl}/seclore/drm/1.0/protect/fileid`, + headers, + body: protectRequest, + json: true, + }; - // Add correlation ID if provided - if (correlationId) { - headers['X-SECLORE-CORRELATION-ID'] = correlationId; - } + try { + const response = await this.context.helpers.httpRequest(options); + return response as IProtectWithFileIdResponse; + } catch (error: unknown) { + this.handleHttpError(error as NodeApiError); + } + } - const options: IHttpRequestOptions = { - method: 'POST', - url: `${this.baseUrl}/seclore/drm/1.0/protect/hf`, - headers, - body: protectRequest, - json: true, - }; + /** + * Protects file using HotFolder ID with PS configured against the logged in Tenant in application. + * + * @param protectRequest - The protection request details with hotfolder ID + * @param accessToken - JWT access token for authorization + * @param correlationId - Optional request ID for logging purpose + * @returns Promise - File storage ID and Seclore file ID + * @throws Error on bad request, authentication failure or server error + */ + async protectWithHotFolder( + protectRequest: IProtectWithHotFolderRequest, + accessToken: string, + correlationId?: string, + ): Promise { + const headers: { [key: string]: string } = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }; - try { - const response = await this.context.helpers.httpRequest(options); - return response as IProtectWithHotFolderResponse; - } catch (error: any) { - this.handleHttpError(error); - } - } + // Add correlation ID if provided + if (correlationId) { + headers['X-SECLORE-CORRELATION-ID'] = correlationId; + } - /** - * Unprotects file with PS configured against the logged in Tenant in application. - * - * @param unprotectRequest - The unprotect request details with file storage ID - * @param accessToken - JWT access token for authorization - * @param correlationId - Optional request ID for logging purpose - * @returns Promise - File storage ID of unprotected file - * @throws Error on bad request, authentication failure or server error - */ - async unprotect( - unprotectRequest: IUnprotectRequest, - accessToken: string, - correlationId?: string - ): Promise { - const headers: { [key: string]: string } = { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }; + const options: IHttpRequestOptions = { + method: 'POST', + url: `${this.baseUrl}/seclore/drm/1.0/protect/hf`, + headers, + body: protectRequest, + json: true, + }; - // Add correlation ID if provided - if (correlationId) { - headers['X-SECLORE-CORRELATION-ID'] = correlationId; - } + try { + const response = await this.context.helpers.httpRequest(options); + return response as IProtectWithHotFolderResponse; + } catch (error: unknown) { + this.handleHttpError(error as NodeApiError); + } + } - const options: IHttpRequestOptions = { - method: 'POST', - url: `${this.baseUrl}/seclore/drm/1.0/unprotect`, - headers, - body: unprotectRequest, - json: true, - }; + /** + * Unprotects file with PS configured against the logged in Tenant in application. + * + * @param unprotectRequest - The unprotect request details with file storage ID + * @param accessToken - JWT access token for authorization + * @param correlationId - Optional request ID for logging purpose + * @returns Promise - File storage ID of unprotected file + * @throws Error on bad request, authentication failure or server error + */ + async unprotect( + unprotectRequest: IUnprotectRequest, + accessToken: string, + correlationId?: string, + ): Promise { + const headers: { [key: string]: string } = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + }; - try { - const response = await this.context.helpers.httpRequest(options); - return response as IUnprotectResponse; - } catch (error: any) { - this.handleHttpError(error); - } - } + // Add correlation ID if provided + if (correlationId) { + headers['X-SECLORE-CORRELATION-ID'] = correlationId; + } - /** - * Adds a new file to the file storage for currently logged in Tenant. - * - * @param fileBuffer - The file buffer data - * @param fileName - The name of the file - * @param accessToken - JWT access token for authorization - * @param correlationId - Optional request ID for logging purpose - * @returns Promise - File storage details including file ID and metadata - * @throws Error on authentication failure, payload too large, or server error - */ - async uploadFile( - fileBuffer: Uint8Array, - fileName: string, - accessToken: string, - correlationId?: string - ): Promise { - const headers: { [key: string]: string } = { - 'Authorization': `Bearer ${accessToken}`, - }; + const options: IHttpRequestOptions = { + method: 'POST', + url: `${this.baseUrl}/seclore/drm/1.0/unprotect`, + headers, + body: unprotectRequest, + json: true, + }; - // Add correlation ID if provided - if (correlationId) { - headers['X-SECLORE-CORRELATION-ID'] = correlationId; - } + try { + const response = await this.context.helpers.httpRequest(options); + return response as IUnprotectResponse; + } catch (error: unknown) { + this.handleHttpError(error as NodeApiError); + } + } - // Create FormData for multipart/form-data upload - const formData = new FormData(); - formData.append('file', fileBuffer, fileName); + /** + * Adds a new file to the file storage for currently logged in Tenant. + * + * @param fileBuffer - The file buffer data + * @param fileName - The name of the file + * @param accessToken - JWT access token for authorization + * @param correlationId - Optional request ID for logging purpose + * @returns Promise - File storage details including file ID and metadata + * @throws Error on authentication failure, payload too large, or server error + */ + async uploadFile( + fileBuffer: Uint8Array, + fileName: string, + accessToken: string, + correlationId?: string, + ): Promise { + const headers: { [key: string]: string } = { + Authorization: `Bearer ${accessToken}`, + }; - const options: IHttpRequestOptions = { - method: 'POST', - url: `${this.baseUrl}/seclore/drm/filestorage/1.0/upload`, - headers, - body: formData, - }; + // Add correlation ID if provided + if (correlationId) { + headers['X-SECLORE-CORRELATION-ID'] = correlationId; + } - try { - const response = await this.context.helpers.httpRequest(options); - return response as IFileUploadResponse; - } catch (error: any) { - this.handleHttpError(error); - } - } + // Create FormData for multipart/form-data upload + const formData = new FormData(); + const file = new Blob([fileBuffer], { type: 'application/octet-stream' }); + formData.append('file', file, fileName); - /** - * 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}`, - }; + const options: IHttpRequestOptions = { + method: 'POST', + url: `${this.baseUrl}/seclore/drm/filestorage/1.0/upload`, + headers, + body: formData, + }; - // Add correlation ID if provided - if (correlationId) { - headers['X-SECLORE-CORRELATION-ID'] = correlationId; - } + try { + const response = await this.context.helpers.httpRequest(options); + return response as IFileUploadResponse; + } catch (error: unknown) { + this.handleHttpError(error as NodeApiError); + } + } - const options: IHttpRequestOptions = { - method: 'GET', - url: `${this.baseUrl}/seclore/drm/filestorage/1.0/download/${fileStorageId}`, - headers, - encoding: 'arraybuffer', - }; + /** + * 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}`, + }; - try { - const response = await this.context.helpers.httpRequest(options); - return new Uint8Array(response as ArrayBuffer); - } catch (error: any) { - this.handleHttpError(error); - } - } + // 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: unknown) { + this.handleHttpError(error as NodeApiError); + } + } } diff --git a/nodes/SecloreProtect/Services/SecloreDRMFileService.ts b/nodes/SecloreProtect/Services/SecloreDRMFileService.ts index ae078cc..7a8192d 100644 --- a/nodes/SecloreProtect/Services/SecloreDRMFileService.ts +++ b/nodes/SecloreProtect/Services/SecloreDRMFileService.ts @@ -1,298 +1,303 @@ 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'; +import { + IProtectWithExternalRefIdRequest, + IProtectWithExternalRefIdResponse, + IProtectWithFileIdRequest, + IProtectWithFileIdResponse, + IProtectWithHotFolderRequest, + IProtectWithHotFolderResponse, +} from './Interfaces/ProtectInterfaces'; +import { IUnprotectRequest, IUnprotectResponse } from './Interfaces/UnprotectInterfaces'; +import { SecloreDRMApiService } from './SecloreDRMApiService'; 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; + 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; - } + 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; - } - } - } + /** + * 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; + } - /** - * 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; - } - } + // Start login and store the promise + this.loginPromise = this.login(correlationId); + try { + await this.loginPromise; + } finally { + this.loginPromise = undefined; + } + } + } - /** - * 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; - } + /** + * 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, + ); - if (!this.refreshToken) { - throw new Error('No refresh token available'); - } + this.accessToken = loginResponse.accessToken; + this.refreshToken = loginResponse.refreshToken; - // Start refresh and store the promise - this.refreshPromise = this.performTokenRefresh(correlationId); - try { - await this.refreshPromise; - } finally { - this.refreshPromise = undefined; - } - } + // 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; + } + } - /** - * 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; - } - } + /** + * 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; + } - /** - * Clears stored tokens and pending promises - */ - private clearTokens(): void { - this.accessToken = undefined; - this.refreshToken = undefined; - this.tokenExpiry = undefined; - this.refreshPromise = undefined; - this.loginPromise = undefined; - } + if (!this.refreshToken) { + throw new Error('No refresh token available'); + } - /** - * 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; + // Start refresh and store the promise + this.refreshPromise = this.performTokenRefresh(correlationId); + try { + await this.refreshPromise; + } finally { + this.refreshPromise = undefined; + } + } - 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'); - } + /** + * 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); - // 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); - }); - } - } + this.accessToken = refreshResponse.accessToken; + this.refreshToken = refreshResponse.refreshToken; - throw lastError!; - } + // Set token expiry to 50 minutes from now + this.tokenExpiry = new Date(Date.now() + 50 * 60 * 1000); + } catch (error) { + this.clearTokens(); + throw error; + } + } - /** - * 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 - ); - } + /** + * Clears stored tokens and pending promises + */ + private clearTokens(): void { + this.accessToken = undefined; + this.refreshToken = undefined; + this.tokenExpiry = undefined; + this.refreshPromise = undefined; + this.loginPromise = undefined; + } - /** - * 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 - ); - } + /** + * 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; - /** - * 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 - ); - } + for (let attempt = 0; attempt <= retryCount; attempt++) { + try { + // Ensure we have a valid token + await this.ensureAuthenticated(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 - ); - } + if (!this.accessToken) { + throw new Error('Failed to obtain access token'); + } - /** - * 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 - ); - } + // Execute the API call + return await apiCall(this.accessToken); + } catch (error: unknown) { + lastError = error as Error; + // If it's an authentication error and we have retries left, try to refresh token + if ((error as Error).message.includes('Unauthorized') && attempt < retryCount) { + try { + await this.refreshAccessToken(correlationId); + continue; // Retry with new token + } catch (error: unknown) { + console.error(error); + // If refresh fails, clear tokens and try full login on next attempt + this.clearTokens(); + } + } - /** - * 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 - ); - } + // If it's the last attempt or not an auth error, throw + if (attempt === retryCount) { + throw lastError; + } - /** - * Get current access token (for debugging/monitoring) - */ - getAccessToken(): string | undefined { - return this.accessToken; - } + // 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); + }); + } + } - /** - * Check if currently authenticated - */ - isAuthenticated(): boolean { - return !!this.accessToken && (!this.tokenExpiry || new Date() < this.tokenExpiry); - } + 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); + } }