POC successful for protect with hf

This commit is contained in:
atharva.dev 2025-10-24 06:10:55 +00:00
parent 9f6db09edf
commit 6e7e28a2e6
10 changed files with 708 additions and 663 deletions

2
.gitignore vendored
View File

@ -1,4 +1,4 @@
dist dist
node_modules node_modules
package-lock.json package-lock.json
.n8n/.env .n8n

View File

@ -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',
},
},
};
}

View File

@ -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',
},
},
};
}

View File

Before

Width:  |  Height:  |  Size: 506 B

After

Width:  |  Height:  |  Size: 506 B

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080">
<defs>
<style>
.cls-1 {
fill: #e6244e;
}
</style>
</defs>
<path class="cls-1" d="M1410.69,1080H336.23v-225.18h983.63v-196.85H538.82c-135.08-7.21-202.59-53.13-202.59-137.81V128.33C336.23,49.07,403.68,6.3,538.62,0H1583.77V230.56H596.13v199.54h813.03c108.4,0,166.61,37.75,174.6,113.27v404.49c-8,88.11-65.68,132.14-173.08,132.14Z"/>
</svg>

After

Width:  |  Height:  |  Size: 506 B

View File

@ -1,3 +0,0 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0165 0C8.94791 0 0 9.01388 0 20.1653C0 29.0792 5.73324 36.6246 13.6868 39.2952C14.6812 39.496 15.0454 38.8613 15.0454 38.3274C15.0454 37.8599 15.0126 36.2575 15.0126 34.5879C9.4445 35.79 8.28498 32.1841 8.28498 32.1841C7.39015 29.847 6.06429 29.2463 6.06429 29.2463C4.24185 28.011 6.19704 28.011 6.19704 28.011C8.21861 28.1446 9.27938 30.081 9.27938 30.081C11.0686 33.1522 13.9518 32.2844 15.1118 31.7502C15.2773 30.4481 15.8079 29.5467 16.3713 29.046C11.9303 28.5785 7.25781 26.8425 7.25781 19.0967C7.25781 16.8932 8.05267 15.0905 9.31216 13.6884C9.11344 13.1877 8.41732 11.1174 9.51128 8.34644C9.51128 8.34644 11.2014 7.81217 15.0122 10.4164C16.6438 9.97495 18.3263 9.7504 20.0165 9.74851C21.7067 9.74851 23.4295 9.98246 25.0205 10.4164C28.8317 7.81217 30.5218 8.34644 30.5218 8.34644C31.6158 11.1174 30.9192 13.1877 30.7205 13.6884C32.0132 15.0905 32.7753 16.8932 32.7753 19.0967C32.7753 26.8425 28.1028 28.5449 23.6287 29.046C24.358 29.6802 24.9873 30.882 24.9873 32.7851C24.9873 35.4893 24.9545 37.6596 24.9545 38.327C24.9545 38.8613 25.3192 39.496 26.3132 39.2956C34.2667 36.6242 39.9999 29.0792 39.9999 20.1653C40.0327 9.01388 31.052 0 20.0165 0Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,3 +0,0 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.0165 0C8.94791 0 0 9.01388 0 20.1653C0 29.0792 5.73324 36.6246 13.6868 39.2952C14.6812 39.496 15.0454 38.8613 15.0454 38.3274C15.0454 37.8599 15.0126 36.2575 15.0126 34.5879C9.4445 35.79 8.28498 32.1841 8.28498 32.1841C7.39015 29.847 6.06429 29.2463 6.06429 29.2463C4.24185 28.011 6.19704 28.011 6.19704 28.011C8.21861 28.1446 9.27938 30.081 9.27938 30.081C11.0686 33.1522 13.9518 32.2844 15.1118 31.7502C15.2773 30.4481 15.8079 29.5467 16.3713 29.046C11.9303 28.5785 7.25781 26.8425 7.25781 19.0967C7.25781 16.8932 8.05267 15.0905 9.31216 13.6884C9.11344 13.1877 8.41732 11.1174 9.51128 8.34644C9.51128 8.34644 11.2014 7.81217 15.0122 10.4164C16.6438 9.97495 18.3263 9.7504 20.0165 9.74851C21.7067 9.74851 23.4295 9.98246 25.0205 10.4164C28.8317 7.81217 30.5218 8.34644 30.5218 8.34644C31.6158 11.1174 30.9192 13.1877 30.7205 13.6884C32.0132 15.0905 32.7753 16.8932 32.7753 19.0967C32.7753 26.8425 28.1028 28.5449 23.6287 29.046C24.358 29.6802 24.9873 30.882 24.9873 32.7851C24.9873 35.4893 24.9545 37.6596 24.9545 38.327C24.9545 38.8613 25.3192 39.496 26.3132 39.2956C34.2667 36.6242 39.9999 29.0792 39.9999 20.1653C40.0327 9.01388 31.052 0 20.0165 0Z" fill="#24292F"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -12,7 +12,8 @@ export class SecloreProtect implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
displayName: 'Seclore Protect', displayName: 'Seclore Protect',
name: 'secloreProtect', 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'], group: ['transform'],
version: 1, version: 1,
subtitle: '={{$parameter["operation"]}}', subtitle: '={{$parameter["operation"]}}',
@ -39,7 +40,7 @@ export class SecloreProtect implements INodeType {
name: 'Protect File with HotFolder', name: 'Protect File with HotFolder',
value: 'protectWithHotFolder', value: 'protectWithHotFolder',
description: 'Protect a file using HotFolder ID configuration', description: 'Protect a file using HotFolder ID configuration',
action: 'Protect file with HotFolder', action: 'Protect file with hotfolder',
}, },
], ],
default: 'protectWithHotFolder', default: 'protectWithHotFolder',
@ -129,6 +130,8 @@ export class SecloreProtect implements INodeType {
const operation = this.getNodeParameter('operation', 0) as string; const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
console.log('Data');
console.log(items[i]);
try { try {
if (operation === 'protectWithHotFolder') { if (operation === 'protectWithHotFolder') {
// Get parameters for this item // Get parameters for this item
@ -148,10 +151,16 @@ export class SecloreProtect implements INodeType {
}); });
} }
console.log('assertBinaryData');
// Get input binary data // Get input binary data
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName); const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
console.log('getBinaryDataBuffer');
const fileBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName); const fileBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
console.log('fileBuffer', fileBuffer);
console.log('binaryData.fileName', binaryData.fileName);
// Upload the file first // Upload the file first
const uploadResult = await fileService.uploadFile( const uploadResult = await fileService.uploadFile(
new Uint8Array(fileBuffer), new Uint8Array(fileBuffer),
@ -160,6 +169,8 @@ export class SecloreProtect implements INodeType {
retryCount, retryCount,
); );
console.log('File upload response', uploadResult);
// Protect the uploaded file with HotFolder // Protect the uploaded file with HotFolder
const protectResult = await fileService.protectWithHotFolder( const protectResult = await fileService.protectWithHotFolder(
{ {
@ -170,6 +181,8 @@ export class SecloreProtect implements INodeType {
retryCount, retryCount,
); );
console.log('File protect response', protectResult);
// Download the protected file // Download the protected file
const protectedFileData = await fileService.downloadFile( const protectedFileData = await fileService.downloadFile(
protectResult.fileStorageId, protectResult.fileStorageId,
@ -177,6 +190,8 @@ export class SecloreProtect implements INodeType {
retryCount, retryCount,
); );
console.log('Protected file data', protectedFileData);
// Create output binary data // Create output binary data
const outputBinaryData = await this.helpers.prepareBinaryData( const outputBinaryData = await this.helpers.prepareBinaryData(
Buffer.from(protectedFileData), Buffer.from(protectedFileData),

View File

@ -1,367 +1,387 @@
import { IExecuteFunctions, IHttpRequestOptions } from 'n8n-workflow'; import { IExecuteFunctions, IHttpRequestOptions, NodeApiError } from 'n8n-workflow';
import { ILoginRequest, ILoginResponse, IRefreshTokenRequest } from './Interfaces/LoginInterfaces';
import { IErrorResponse } from './Interfaces/ErrorInterfaces'; 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 { 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 { export class SecloreDRMApiService {
private baseUrl: string; private baseUrl: string;
constructor(private context: IExecuteFunctions, baseUrl: string) { constructor(
this.baseUrl = baseUrl; private context: IExecuteFunctions,
} baseUrl: string,
) {
this.baseUrl = baseUrl;
}
/** /**
* Common error handler for HTTP responses * Common error handler for HTTP responses
* @param error - The error object from httpRequest * @param error - The error object from httpRequest
* @param customMessages - Optional custom error messages for specific status codes * @param customMessages - Optional custom error messages for specific status codes
*/ */
private handleHttpError(error: any, customMessages?: { [statusCode: number]: string }): never { private handleHttpError(
const statusCode = error.statusCode; error: NodeApiError,
const errorResponse = error.response?.body as IErrorResponse; customMessages?: { [statusCode: number]: string },
): never {
const statusCode: number = parseInt(error.httpCode ?? '0');
const errorResponse = error.errorResponse as unknown as IErrorResponse;
if (customMessages && customMessages[statusCode]) { if (customMessages && customMessages[statusCode]) {
throw new Error(`${customMessages[statusCode]}: ${errorResponse?.errorMessage || 'Unknown error'}`); throw new Error(
} `${customMessages[statusCode]}: ${errorResponse?.errorMessage || 'Unknown error'}`,
);
}
// Default error handling // Default error handling
switch (statusCode) { switch (statusCode) {
case 400: case 400:
throw new Error(`Bad Request: ${errorResponse?.errorMessage || 'Invalid request data'}`); throw new Error(`Bad Request: ${errorResponse?.errorMessage || 'Invalid request data'}`);
case 401: case 401:
throw new Error(`Unauthorized: ${errorResponse?.errorMessage || 'Invalid credentials'}`); throw new Error(`Unauthorized: ${errorResponse?.errorMessage || 'Invalid credentials'}`);
case 413: case 413:
throw new Error(`Payload Too Large: ${errorResponse?.errorMessage || 'File size exceeds limit'}`); throw new Error(
case 500: `Payload Too Large: ${errorResponse?.errorMessage || 'File size exceeds limit'}`,
throw new Error(`Server Error: ${errorResponse?.errorMessage || 'Internal server error'}`); );
default: case 500:
throw error; throw new Error(`Server Error: ${errorResponse?.errorMessage || 'Internal server error'}`);
} default:
} throw error;
}
}
/** /**
* Login Endpoint to generate Access Token and Refresh Token for JWT Authorization. * 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. * Upon successful login, all the existing previous tokens for that tenant will be invalidated.
* *
* @param tenantId - The tenant ID * @param tenantId - The tenant ID
* @param tenantSecret - The tenant secret * @param tenantSecret - The tenant secret
* @param correlationId - Optional request ID for logging purpose * @param correlationId - Optional request ID for logging purpose
* @returns Promise<ILoginResponse> - Access token and refresh token * @returns Promise<ILoginResponse> - Access token and refresh token
* @throws Error on authentication failure or server error * @throws Error on authentication failure or server error
*/ */
async login(tenantId: string, tenantSecret: string, correlationId?: string): Promise<ILoginResponse> { async login(
const requestBody: ILoginRequest = { tenantId: string,
tenantId, tenantSecret: string,
tenantSecret, correlationId?: string,
}; ): Promise<ILoginResponse> {
const requestBody: ILoginRequest = {
tenantId,
tenantSecret,
};
const headers: { [key: string]: string } = { const headers: { [key: string]: string } = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
// Add correlation ID if provided // Add correlation ID if provided
if (correlationId) { if (correlationId) {
headers['X-SECLORE-CORRELATION-ID'] = correlationId; headers['X-SECLORE-CORRELATION-ID'] = correlationId;
} }
const options: IHttpRequestOptions = { const options: IHttpRequestOptions = {
method: 'POST', method: 'POST',
url: `${this.baseUrl}/seclore/drm/1.0/auth/login`, url: `${this.baseUrl}/seclore/drm/1.0/auth/login`,
headers, headers,
body: requestBody, body: requestBody,
json: true, json: true,
}; };
try { try {
const response = await this.context.helpers.httpRequest(options); const response = await this.context.helpers.httpRequest(options);
return response as ILoginResponse; return response as ILoginResponse;
} catch (error: any) { } catch (error: unknown) {
this.handleHttpError(error); this.handleHttpError(error as NodeApiError);
} }
} }
/** /**
* Endpoint for generating new Access Token and Refresh Token using an existing valid Refresh Token. * 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. * 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 refreshToken - The existing valid refresh token
* @param correlationId - Optional request ID for logging purpose * @param correlationId - Optional request ID for logging purpose
* @returns Promise<ILoginResponse> - New access token and refresh token * @returns Promise<ILoginResponse> - New access token and refresh token
* @throws Error on authentication failure or server error * @throws Error on authentication failure or server error
*/ */
async refreshToken(refreshToken: string, correlationId?: string): Promise<ILoginResponse> { async refreshToken(refreshToken: string, correlationId?: string): Promise<ILoginResponse> {
const requestBody: IRefreshTokenRequest = { const requestBody: IRefreshTokenRequest = {
refreshToken, refreshToken,
}; };
const headers: { [key: string]: string } = { const headers: { [key: string]: string } = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
// Add correlation ID if provided // Add correlation ID if provided
if (correlationId) { if (correlationId) {
headers['X-SECLORE-CORRELATION-ID'] = correlationId; headers['X-SECLORE-CORRELATION-ID'] = correlationId;
} }
const options: IHttpRequestOptions = { const options: IHttpRequestOptions = {
method: 'POST', method: 'POST',
url: `${this.baseUrl}/seclore/drm/1.0/auth/refresh`, url: `${this.baseUrl}/seclore/drm/1.0/auth/refresh`,
headers, headers,
body: requestBody, body: requestBody,
json: true, json: true,
}; };
try { try {
const response = await this.context.helpers.httpRequest(options); const response = await this.context.helpers.httpRequest(options);
return response as ILoginResponse; return response as ILoginResponse;
} catch (error: any) { } catch (error: unknown) {
this.handleHttpError(error, { 401: 'Unauthorized' }); this.handleHttpError(error as NodeApiError, { 401: 'Unauthorized' });
} }
} }
/** /**
* Protect file using external identifier of protected File and HotFolder with PS configured against the logged in Tenant in application. * 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 protectRequest - The protection request details
* @param accessToken - JWT access token for authorization * @param accessToken - JWT access token for authorization
* @param correlationId - Optional request ID for logging purpose * @param correlationId - Optional request ID for logging purpose
* @returns Promise<IProtectWithExternalRefIdResponse> - File storage ID and Seclore file ID * @returns Promise<IProtectWithExternalRefIdResponse> - File storage ID and Seclore file ID
* @throws Error on bad request, authentication failure or server error * @throws Error on bad request, authentication failure or server error
*/ */
async protectWithExternalRefId( async protectWithExternalRefId(
protectRequest: IProtectWithExternalRefIdRequest, protectRequest: IProtectWithExternalRefIdRequest,
accessToken: string, accessToken: string,
correlationId?: string correlationId?: string,
): Promise<IProtectWithExternalRefIdResponse> { ): Promise<IProtectWithExternalRefIdResponse> {
const headers: { [key: string]: string } = { const headers: { [key: string]: string } = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
}; };
// Add correlation ID if provided // Add correlation ID if provided
if (correlationId) { if (correlationId) {
headers['X-SECLORE-CORRELATION-ID'] = correlationId; headers['X-SECLORE-CORRELATION-ID'] = correlationId;
} }
const options: IHttpRequestOptions = { const options: IHttpRequestOptions = {
method: 'POST', method: 'POST',
url: `${this.baseUrl}/seclore/drm/1.0/protect/externalref`, url: `${this.baseUrl}/seclore/drm/1.0/protect/externalref`,
headers, headers,
body: protectRequest, body: protectRequest,
json: true, json: true,
}; };
try { try {
const response = await this.context.helpers.httpRequest(options); const response = await this.context.helpers.httpRequest(options);
return response as IProtectWithExternalRefIdResponse; return response as IProtectWithExternalRefIdResponse;
} catch (error: any) { } catch (error: unknown) {
this.handleHttpError(error); this.handleHttpError(error as NodeApiError);
} }
} }
/** /**
* Protects file using File ID of already protected file with PS configured against the logged in Tenant in application. * 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 protectRequest - The protection request details with existing protected file ID
* @param accessToken - JWT access token for authorization * @param accessToken - JWT access token for authorization
* @param correlationId - Optional request ID for logging purpose * @param correlationId - Optional request ID for logging purpose
* @returns Promise<IProtectWithFileIdResponse> - File storage ID and Seclore file ID * @returns Promise<IProtectWithFileIdResponse> - File storage ID and Seclore file ID
* @throws Error on bad request, authentication failure or server error * @throws Error on bad request, authentication failure or server error
*/ */
async protectWithFileId( async protectWithFileId(
protectRequest: IProtectWithFileIdRequest, protectRequest: IProtectWithFileIdRequest,
accessToken: string, accessToken: string,
correlationId?: string correlationId?: string,
): Promise<IProtectWithFileIdResponse> { ): Promise<IProtectWithFileIdResponse> {
const headers: { [key: string]: string } = { const headers: { [key: string]: string } = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
}; };
// Add correlation ID if provided // Add correlation ID if provided
if (correlationId) { if (correlationId) {
headers['X-SECLORE-CORRELATION-ID'] = correlationId; headers['X-SECLORE-CORRELATION-ID'] = correlationId;
} }
const options: IHttpRequestOptions = { const options: IHttpRequestOptions = {
method: 'POST', method: 'POST',
url: `${this.baseUrl}/seclore/drm/1.0/protect/fileid`, url: `${this.baseUrl}/seclore/drm/1.0/protect/fileid`,
headers, headers,
body: protectRequest, body: protectRequest,
json: true, json: true,
}; };
try { try {
const response = await this.context.helpers.httpRequest(options); const response = await this.context.helpers.httpRequest(options);
return response as IProtectWithFileIdResponse; return response as IProtectWithFileIdResponse;
} catch (error: any) { } catch (error: unknown) {
this.handleHttpError(error); this.handleHttpError(error as NodeApiError);
} }
} }
/** /**
* Protects file using HotFolder ID with PS configured against the logged in Tenant in application. * 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 protectRequest - The protection request details with hotfolder ID
* @param accessToken - JWT access token for authorization * @param accessToken - JWT access token for authorization
* @param correlationId - Optional request ID for logging purpose * @param correlationId - Optional request ID for logging purpose
* @returns Promise<IProtectWithHotFolderResponse> - File storage ID and Seclore file ID * @returns Promise<IProtectWithHotFolderResponse> - File storage ID and Seclore file ID
* @throws Error on bad request, authentication failure or server error * @throws Error on bad request, authentication failure or server error
*/ */
async protectWithHotFolder( async protectWithHotFolder(
protectRequest: IProtectWithHotFolderRequest, protectRequest: IProtectWithHotFolderRequest,
accessToken: string, accessToken: string,
correlationId?: string correlationId?: string,
): Promise<IProtectWithHotFolderResponse> { ): Promise<IProtectWithHotFolderResponse> {
const headers: { [key: string]: string } = { const headers: { [key: string]: string } = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
}; };
// Add correlation ID if provided // Add correlation ID if provided
if (correlationId) { if (correlationId) {
headers['X-SECLORE-CORRELATION-ID'] = correlationId; headers['X-SECLORE-CORRELATION-ID'] = correlationId;
} }
const options: IHttpRequestOptions = { const options: IHttpRequestOptions = {
method: 'POST', method: 'POST',
url: `${this.baseUrl}/seclore/drm/1.0/protect/hf`, url: `${this.baseUrl}/seclore/drm/1.0/protect/hf`,
headers, headers,
body: protectRequest, body: protectRequest,
json: true, json: true,
}; };
try { try {
const response = await this.context.helpers.httpRequest(options); const response = await this.context.helpers.httpRequest(options);
return response as IProtectWithHotFolderResponse; return response as IProtectWithHotFolderResponse;
} catch (error: any) { } catch (error: unknown) {
this.handleHttpError(error); this.handleHttpError(error as NodeApiError);
} }
} }
/** /**
* Unprotects file with PS configured against the logged in Tenant in application. * Unprotects file with PS configured against the logged in Tenant in application.
* *
* @param unprotectRequest - The unprotect request details with file storage ID * @param unprotectRequest - The unprotect request details with file storage ID
* @param accessToken - JWT access token for authorization * @param accessToken - JWT access token for authorization
* @param correlationId - Optional request ID for logging purpose * @param correlationId - Optional request ID for logging purpose
* @returns Promise<IUnprotectResponse> - File storage ID of unprotected file * @returns Promise<IUnprotectResponse> - File storage ID of unprotected file
* @throws Error on bad request, authentication failure or server error * @throws Error on bad request, authentication failure or server error
*/ */
async unprotect( async unprotect(
unprotectRequest: IUnprotectRequest, unprotectRequest: IUnprotectRequest,
accessToken: string, accessToken: string,
correlationId?: string correlationId?: string,
): Promise<IUnprotectResponse> { ): Promise<IUnprotectResponse> {
const headers: { [key: string]: string } = { const headers: { [key: string]: string } = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
}; };
// Add correlation ID if provided // Add correlation ID if provided
if (correlationId) { if (correlationId) {
headers['X-SECLORE-CORRELATION-ID'] = correlationId; headers['X-SECLORE-CORRELATION-ID'] = correlationId;
} }
const options: IHttpRequestOptions = { const options: IHttpRequestOptions = {
method: 'POST', method: 'POST',
url: `${this.baseUrl}/seclore/drm/1.0/unprotect`, url: `${this.baseUrl}/seclore/drm/1.0/unprotect`,
headers, headers,
body: unprotectRequest, body: unprotectRequest,
json: true, json: true,
}; };
try { try {
const response = await this.context.helpers.httpRequest(options); const response = await this.context.helpers.httpRequest(options);
return response as IUnprotectResponse; return response as IUnprotectResponse;
} catch (error: any) { } catch (error: unknown) {
this.handleHttpError(error); this.handleHttpError(error as NodeApiError);
} }
} }
/** /**
* Adds a new file to the file storage for currently logged in Tenant. * Adds a new file to the file storage for currently logged in Tenant.
* *
* @param fileBuffer - The file buffer data * @param fileBuffer - The file buffer data
* @param fileName - The name of the file * @param fileName - The name of the file
* @param accessToken - JWT access token for authorization * @param accessToken - JWT access token for authorization
* @param correlationId - Optional request ID for logging purpose * @param correlationId - Optional request ID for logging purpose
* @returns Promise<IFileUploadResponse> - File storage details including file ID and metadata * @returns Promise<IFileUploadResponse> - File storage details including file ID and metadata
* @throws Error on authentication failure, payload too large, or server error * @throws Error on authentication failure, payload too large, or server error
*/ */
async uploadFile( async uploadFile(
fileBuffer: Uint8Array, fileBuffer: Uint8Array,
fileName: string, fileName: string,
accessToken: string, accessToken: string,
correlationId?: string correlationId?: string,
): Promise<IFileUploadResponse> { ): Promise<IFileUploadResponse> {
const headers: { [key: string]: string } = { const headers: { [key: string]: string } = {
'Authorization': `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
}; };
// Add correlation ID if provided // Add correlation ID if provided
if (correlationId) { if (correlationId) {
headers['X-SECLORE-CORRELATION-ID'] = correlationId; headers['X-SECLORE-CORRELATION-ID'] = correlationId;
} }
// Create FormData for multipart/form-data upload // Create FormData for multipart/form-data upload
const formData = new FormData(); const formData = new FormData();
formData.append('file', fileBuffer, fileName); const file = new Blob([fileBuffer], { type: 'application/octet-stream' });
formData.append('file', file, fileName);
const options: IHttpRequestOptions = { const options: IHttpRequestOptions = {
method: 'POST', method: 'POST',
url: `${this.baseUrl}/seclore/drm/filestorage/1.0/upload`, url: `${this.baseUrl}/seclore/drm/filestorage/1.0/upload`,
headers, headers,
body: formData, body: formData,
}; };
try { try {
const response = await this.context.helpers.httpRequest(options); const response = await this.context.helpers.httpRequest(options);
return response as IFileUploadResponse; return response as IFileUploadResponse;
} catch (error: any) { } catch (error: unknown) {
this.handleHttpError(error); this.handleHttpError(error as NodeApiError);
} }
} }
/** /**
* Downloads file with fileStorageId from file storage of currently logged in Tenant. * 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. * 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 fileStorageId - Storage ID of the file to be retrieved
* @param accessToken - JWT access token for authorization * @param accessToken - JWT access token for authorization
* @param correlationId - Optional request ID for logging purpose * @param correlationId - Optional request ID for logging purpose
* @returns Promise<Uint8Array> - The downloaded file data * @returns Promise<Uint8Array> - The downloaded file data
* @throws Error on authentication failure or server error * @throws Error on authentication failure or server error
*/ */
async downloadFile( async downloadFile(
fileStorageId: string, fileStorageId: string,
accessToken: string, accessToken: string,
correlationId?: string correlationId?: string,
): Promise<Uint8Array> { ): Promise<Uint8Array> {
const headers: { [key: string]: string } = { const headers: { [key: string]: string } = {
'Authorization': `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
}; };
// Add correlation ID if provided // Add correlation ID if provided
if (correlationId) { if (correlationId) {
headers['X-SECLORE-CORRELATION-ID'] = correlationId; headers['X-SECLORE-CORRELATION-ID'] = correlationId;
} }
const options: IHttpRequestOptions = { const options: IHttpRequestOptions = {
method: 'GET', method: 'GET',
url: `${this.baseUrl}/seclore/drm/filestorage/1.0/download/${fileStorageId}`, url: `${this.baseUrl}/seclore/drm/filestorage/1.0/download/${fileStorageId}`,
headers, headers,
encoding: 'arraybuffer', encoding: 'arraybuffer',
}; };
try {
const response = await this.context.helpers.httpRequest(options);
return new Uint8Array(response as ArrayBuffer);
} catch (error: any) {
this.handleHttpError(error);
}
}
try {
const response = await this.context.helpers.httpRequest(options);
return new Uint8Array(response as ArrayBuffer);
} catch (error: unknown) {
this.handleHttpError(error as NodeApiError);
}
}
} }

View File

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