(PS-378) Seclore custom n8n node #1

Merged
atharva.dev merged 22 commits from ticket/PS-378 into main 2025-11-13 04:43:55 +00:00
24 changed files with 2421 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
dist
node_modules
package-lock.json
.n8n
.devcontainer

51
.prettierrc.js Normal file
View File

@ -0,0 +1,51 @@
module.exports = {
/**
* https://prettier.io/docs/en/options.html#semicolons
*/
semi: true,
/**
* https://prettier.io/docs/en/options.html#trailing-commas
*/
trailingComma: 'all',
/**
* https://prettier.io/docs/en/options.html#bracket-spacing
*/
bracketSpacing: true,
/**
* https://prettier.io/docs/en/options.html#tabs
*/
useTabs: true,
/**
* https://prettier.io/docs/en/options.html#tab-width
*/
tabWidth: 2,
/**
* https://prettier.io/docs/en/options.html#arrow-function-parentheses
*/
arrowParens: 'always',
/**
* https://prettier.io/docs/en/options.html#quotes
*/
singleQuote: true,
/**
* https://prettier.io/docs/en/options.html#quote-props
*/
quoteProps: 'as-needed',
/**
* https://prettier.io/docs/en/options.html#end-of-line
*/
endOfLine: 'lf',
/**
* https://prettier.io/docs/en/options.html#print-width
*/
printWidth: 100,
};

0
CHANGELOG.md Normal file
View File

76
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at jan@n8n.io. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

58
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,58 @@
# Contributing
## Workflow
A **feature branch workflow** is followed for this repository. All development work is done on ticket branches that are merged directly into the `main` branch.
## Pre-requisite
- Verify that you have local git setup as mentioned in **[this guide](https://repo.seclore.com/common/user_guides/src/branch/main/docs/coding_processes.md#setting-up-base-machine)**
- A JIRA ticket should be created for the work you plan to contribute
## Contribution Process
### Branch Structure
- **`main`**: Production-ready code. This branch should always be stable and deployable. All ticket branches are created from and merged into this branch.
### Branch Naming Convention
Create branches using the following naming structure:
- **Format**: `ticket/<JIRA_TICKET_ID>`
Examples:
- `ticket/PS-378`
- `ticket/PS-379`
### Step-by-Step Contribution Process
1. **Create a branch from `main`**
```bash
git checkout main
git pull origin main
git checkout -b ticket/<JIRA_TICKET_ID>
```
2. **Follow standard development process**:
- Effort Estimates
- Design Documentation (if required)
- Test Cases
- Code Implementation (following project coding standards)
3. **Raise a Pull Request**
- Create a PR **from your ticket branch into the `main` branch**
- PR title format: `(<JIRA_TICKET_ID>) <short description>`
- Add relevant documentation links in the PR description
- Update the PR link in the JIRA ticket comments
4. **Code Review**
- Address review comments
- Ensure all CI checks pass
- Get required approvals
5. **Merge to Main**
- Upon PR approval, **squash and merge** the ticket branch into `main`
- Delete the ticket branch after successful merge
---

19
LICENSE.md Normal file
View File

@ -0,0 +1,19 @@
Copyright 2022 n8n
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

135
README.md
View File

@ -1,2 +1,137 @@
# n8n-nodes-seclore
This is an n8n community node. It lets you use Seclore DRM Server in your n8n workflows.
[Seclore](https://www.seclore.com) is a leading data-centric security platform that provides enterprise-grade Digital Rights Management (DRM) capabilities to protect sensitive files and documents throughout their lifecycle.
[n8n](https://n8n.io/) is a [fair-code licensed](https://docs.n8n.io/reference/license/) workflow automation platform.
[Installation](#installation)
[Operations](#operations)
[Credentials](#credentials)
[Compatibility](#compatibility)
[Usage](#usage)
[Resources](#resources)
## Installation
Follow the [installation guide](https://docs.n8n.io/integrations/community-nodes/installation/) in the n8n community nodes documentation.
Use `n8n-nodes-seclore` as the package name.
## Operations
This node supports the following operations with Seclore DRM Server:
### DRM Protection
#### Protect using Policy
- **Description**: Protects files using Seclore's HotFolder ID configuration and policy-based protection
- **Required Parameters**:
- **HotFolder ID**: The ID of the HotFolder configuration to use for protection
- **Input Binary Property**: Name of the binary property containing the file to protect (default: 'data')
- **Retry Count**: Number of retry attempts for failed requests (default: 3)
- **Output**: Protected file with DRM protection applied according to the specified policy
### DRM Unprotection
#### Unprotect
- **Description**: Removes DRM protection from Seclore-protected files
- **Required Parameters**:
- **Input Binary Property**: Name of the binary property containing the protected file (default: 'data')
- **Retry Count**: Number of retry attempts for failed requests (default: 3)
- **Output**: Unprotected original file content
## Credentials
To use this node, you need to authenticate with your Seclore DRM Server. The following credentials are required:
### Prerequisites
- Access to a Seclore DRM Server instance
- Valid tenant credentials (Tenant ID and Tenant Secret)
- Appropriate permissions for file protection/unprotection operations
### Authentication Setup
1. **Base URL**: The base URL of your Seclore DRM Server (e.g., `https://api.seclore.com`)
2. **Tenant ID**: Your unique Seclore tenant identifier
3. **Tenant Secret**: Your tenant's secret key for API authentication
### Configuration Steps
1. In n8n, go to **Credentials** → **Create New**
2. Search for "Seclore API" and select it
3. Fill in your Seclore DRM Server details:
- **Base URL**: Enter your Seclore DRM Server base URL
- **Tenant ID**: Provide your unique tenant identifier
- **Tenant Secret**: Enter your tenant's secret key (will be masked for security)
4. Test the connection to ensure credentials are valid (tests against `/seclore/drm/1.0/auth/login` endpoint)
5. Save the credentials for use in your workflows
## Compatibility
- **Minimum n8n version**: 1.0.0
- **Tested with n8n versions**: 1.0.0+
- **Node.js version**: 18.x or higher
- **Seclore API version**: Compatible with Seclore DRM API v1.0
## Usage
### Basic Workflow Examples
#### Protecting Files with Policy
1. Use a trigger node to receive files (HTTP Request, File Trigger, etc.)
2. Add the **Seclore** node to your workflow
3. Configure the node:
- **Resource**: Select "DRM Protection"
- **Operation**: Select "Protect using Policy"
- **HotFolder ID**: Enter your HotFolder configuration ID
- **Input Binary Property**: Specify the binary property name (default: 'data')
- **Retry Count**: Set retry attempts (default: 3)
4. Configure your Seclore API credentials
5. The node will return the protected file with DRM encryption applied according to your policy
#### Unprotecting Files
1. Receive protected Seclore files in your workflow
2. Add the **Seclore** node to your workflow
3. Configure the node:
- **Resource**: Select "DRM Unprotection"
- **Operation**: Select "Unprotect"
- **Input Binary Property**: Specify the binary property containing the protected file (default: 'data')
- **Retry Count**: Set retry attempts (default: 3)
4. Configure your Seclore API credentials
5. The node will return the original unprotected file content
### Important Notes
- Ensure your Seclore DRM Server is accessible from your n8n instance
- File operations may take time depending on file size and server performance
- Always handle errors appropriately in your workflows
- Protected files can only be unprotected by authorized users with proper credentials
- HotFolder ID must be configured in your Seclore Policy Server before using protection operations
- The node supports binary data processing for both input and output files
- Retry mechanism is built-in for handling temporary network or server issues
### Error Handling
The node includes comprehensive error handling and logging:
- Connection failures to Seclore server
- Authentication errors
- File processing errors
- Automatic cleanup of temporary files
## Resources
* [n8n community nodes documentation](https://docs.n8n.io/integrations/#community-nodes)
* [Seclore Official Documentation](https://docs.seclore.com/)
* [Seclore Support](https://support.seclore.com/)
## License
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
## Support
For issues related to this n8n node:
- Create an issue in this repository
- Contact: support@seclore.com
For Seclore platform support:
- Visit [Seclore Support Portal](https://support.seclore.com/)
- Check [Seclore Documentation](https://docs.seclore.com/)

View File

@ -0,0 +1,55 @@
import { ICredentialTestRequest, ICredentialType, INodeProperties, Icon } from 'n8n-workflow';
export class SecloreProtectApi implements ICredentialType {
name = 'secloreProtectApi';
displayName = 'Seclore API';
documentationUrl = 'https://docs.seclore.com/';
icon: Icon = 'file:../icons/seclore.svg';
properties: INodeProperties[] = [
{
displayName: 'Base URL',
name: 'baseUrl',
type: 'string',
default: '',
placeholder: 'https://api.seclore.com',
description: 'The base URL of your Seclore DRM Server',
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',
},
},
};
}

3
eslint.config.mjs Normal file
View File

@ -0,0 +1,3 @@
import { config } from '@n8n/node-cli/eslint';
export default config;

11
icons/seclore.svg Normal file
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

@ -0,0 +1,4 @@
{
"node": "dist/nodes/SecloreProtect/SecloreProtect.node.js",
"credentials": "dist/credentials/SecloreProtectApi.credentials.js"
}

View File

@ -0,0 +1,172 @@
import {
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { protectWithHotFolder } from './operations/protectWithHotFolder';
import { unprotect } from './operations/unprotect';
export class SecloreProtect implements INodeType {
description: INodeTypeDescription = {
displayName: 'Seclore',
name: 'secloreProtect',
icon: 'file:../../icons/seclore.svg',
usableAsTool: true,
group: ['transform'],
version: 1,
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
description: 'Protect files using Seclore DRM',
defaults: {
name: 'Seclore',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'secloreProtectApi',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'DRM Protection',
value: 'drmProtection',
description: 'DRM file protection operations',
},
{
name: 'DRM Unprotection',
value: 'drmUnprotection',
description: 'DRM file unprotection operations',
},
],
default: 'drmProtection',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['drmProtection'],
},
},
options: [
{
name: 'Protect Using Policy',
value: 'protectWithHotFolder',
description: 'Protect a file using HotFolder ID configuration',
action: 'Protect file using policy',
},
],
default: 'protectWithHotFolder',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['drmUnprotection'],
},
},
options: [
{
name: 'Unprotect',
value: 'unprotect',
description: 'Unprotect a protected file',
action: 'Unprotect file',
},
],
default: 'unprotect',
},
{
displayName: 'HotFolder ID',
name: 'hotfolderId',
type: 'string',
required: true,
default: '',
placeholder: '',
description: 'The ID of the HotFolder configuration to use for protection',
displayOptions: {
show: {
resource: ['drmProtection'],
operation: ['protectWithHotFolder'],
},
},
},
{
displayName: 'Input Binary Property',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
description: 'Name of the binary property that contains the file to protect',
displayOptions: {
show: {
resource: ['drmProtection'],
operation: ['protectWithHotFolder'],
},
},
},
{
displayName: 'Retry Count',
name: 'retryCount',
type: 'number',
default: 3,
description: 'Number of retry attempts for failed requests',
displayOptions: {
show: {
resource: ['drmProtection'],
operation: ['protectWithHotFolder'],
},
},
},
// Unprotect operation parameters
{
displayName: 'Input Binary Property',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
description: 'Name of the binary property that contains the protected file to unprotect',
displayOptions: {
show: {
resource: ['drmUnprotection'],
operation: ['unprotect'],
},
},
},
{
displayName: 'Retry Count',
name: 'retryCount',
type: 'number',
default: 3,
description: 'Number of retry attempts for failed requests',
displayOptions: {
show: {
resource: ['drmUnprotection'],
operation: ['unprotect'],
},
},
},
],
};
customOperations = {
drmProtection: {
protectWithHotFolder,
},
drmUnprotection: {
unprotect,
},
};
}

View File

@ -0,0 +1,4 @@
export interface IErrorResponse {
errorCode: string;
errorMessage: string;
}

View File

@ -0,0 +1,19 @@
export interface IFileUploadResponse {
fileStorageId: string;
fileName: string;
downloadUrl: string;
fileType: string;
fileSize: number;
secloreFileId: string;
protected: boolean;
headers?: { [key: string]: string };
}
export interface IFileDownloadResponse {
data: Uint8Array;
headers?: { [key: string]: string };
}
export interface IFileDeleteResponse {
headers?: { [key: string]: string };
}

View File

@ -0,0 +1,14 @@
export interface ILoginRequest {
tenantId: string;
tenantSecret: string;
}
export interface ILoginResponse {
accessToken: string;
refreshToken: string;
headers?: { [key: string]: string };
}
export interface IRefreshTokenRequest {
refreshToken: string;
}

View File

@ -0,0 +1,40 @@
export interface IExtRefProtectionDetail {
externalReferenceId: string;
externalReferenceName?: string;
externalReferenceData?: string;
externalAppId?: string;
}
export interface IProtectWithExternalRefIdRequest {
hotfolderExternalReference: IExtRefProtectionDetail;
fileExternalReference?: IExtRefProtectionDetail;
fileStorageId: string;
}
export interface IProtectWithExternalRefIdResponse {
fileStorageId: string;
secloreFileId: string;
headers?: { [key: string]: string };
}
export interface IProtectWithFileIdRequest {
existingProtectedFileId: string;
fileStorageId: string;
}
export interface IProtectWithFileIdResponse {
fileStorageId: string;
secloreFileId: string;
headers?: { [key: string]: string };
}
export interface IProtectWithHotFolderRequest {
hotfolderId: string;
fileStorageId: string;
}
export interface IProtectWithHotFolderResponse {
fileStorageId: string;
secloreFileId: string;
headers?: { [key: string]: string };
}

View File

@ -0,0 +1,8 @@
export interface IUnprotectRequest {
fileStorageId: string;
}
export interface IUnprotectResponse {
fileStorageId: string;
headers?: { [key: string]: string };
}

View File

@ -0,0 +1,610 @@
import { IExecuteFunctions, IHttpRequestOptions, NodeApiError, LoggerProxy as Logger } from 'n8n-workflow';
import { IErrorResponse } from './Interfaces/ErrorInterfaces';
import { IFileUploadResponse, IFileDownloadResponse, IFileDeleteResponse } 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 {
constructor(
private context: IExecuteFunctions,
private baseUrl: string,
) { }
/**
* 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;
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;
}
}
/**
* 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<ILoginResponse> - Access token and refresh token
* @throws Error on authentication failure or server error
*/
async login(
tenantId: string,
tenantSecret: string,
correlationId?: string,
): Promise<ILoginResponse> {
const who = "SecloreDRMApiService::login:: ";
try {
Logger.debug(who + 'Attempting login', { tenantId, correlationId });
const requestBody: ILoginRequest = {
tenantId,
tenantSecret,
};
const headers: { [key: string]: string } = {
'Content-Type': 'application/json',
};
// Add correlation ID if provided
if (correlationId) {
headers['X-SECLORE-CORRELATION-ID'] = correlationId;
}
const options: IHttpRequestOptions = {
method: 'POST',
url: `${this.baseUrl}/seclore/drm/1.0/auth/login`,
headers,
body: requestBody,
json: true,
};
Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId });
const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true });
Logger.debug(who + 'Login successful', { tenantId, correlationId });
const loginResponse: ILoginResponse = {
...(response.body as ILoginResponse),
headers: response.headers as { [key: string]: string }
};
return loginResponse;
} catch (error: unknown) {
Logger.error(who + 'Login failed', { error, tenantId, correlationId });
this.handleHttpError(error as NodeApiError);
}
}
/**
* 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<ILoginResponse> - New access token and refresh token
* @throws Error on authentication failure or server error
*/
async refreshToken(refreshToken: string, correlationId?: string): Promise<ILoginResponse> {
const who = "SecloreDRMApiService::refreshToken:: ";
try {
Logger.debug(who + 'Attempting token refresh', { correlationId });
const requestBody: IRefreshTokenRequest = {
refreshToken,
};
const headers: { [key: string]: string } = {
'Content-Type': 'application/json',
};
// Add correlation ID if provided
if (correlationId) {
headers['X-SECLORE-CORRELATION-ID'] = correlationId;
}
const options: IHttpRequestOptions = {
method: 'POST',
url: `${this.baseUrl}/seclore/drm/1.0/auth/refresh`,
headers,
body: requestBody,
json: true,
};
Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId });
const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true });
Logger.debug(who + 'Token refresh successful', { correlationId });
const refreshResponse: ILoginResponse = {
...(response.body as ILoginResponse),
headers: response.headers as { [key: string]: string }
};
return refreshResponse;
} catch (error: unknown) {
Logger.error(who + 'Token refresh failed', { error, correlationId });
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.
*
* @param protectRequest - The protection request details
* @param accessToken - JWT access token for authorization
* @param correlationId - Optional request ID for logging purpose
* @returns Promise<IProtectWithExternalRefIdResponse> - 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<IProtectWithExternalRefIdResponse> {
const who = "SecloreDRMApiService::protectWithExternalRefId:: ";
try {
Logger.debug(who + 'Protecting file with external ref ID', {
fileStorageId: protectRequest.fileStorageId,
hotfolderExternalReferenceId: protectRequest.hotfolderExternalReference.externalReferenceId,
correlationId
});
const headers: { [key: string]: string } = {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
};
// Add correlation ID if provided
if (correlationId) {
headers['X-SECLORE-CORRELATION-ID'] = correlationId;
}
const options: IHttpRequestOptions = {
method: 'POST',
url: `${this.baseUrl}/seclore/drm/1.0/protect/externalref`,
headers,
body: protectRequest,
json: true,
};
Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId });
const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true });
Logger.debug(who + 'Protection with external ref ID successful', {
fileStorageId: protectRequest.fileStorageId,
secloreFileId: (response.body as IProtectWithExternalRefIdResponse).secloreFileId,
correlationId
});
const protectResponse: IProtectWithExternalRefIdResponse = {
...(response.body as IProtectWithExternalRefIdResponse),
headers: response.headers as { [key: string]: string }
};
return protectResponse;
} catch (error: unknown) {
Logger.error(who + 'Protection with external ref ID failed', {
error,
fileStorageId: protectRequest.fileStorageId,
correlationId
});
this.handleHttpError(error as NodeApiError);
}
}
/**
* 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<IProtectWithFileIdResponse> - 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<IProtectWithFileIdResponse> {
const who = "SecloreDRMApiService::protectWithFileId:: ";
try {
Logger.debug(who + 'Protecting file with file ID', {
existingProtectedFileId: protectRequest.existingProtectedFileId,
fileStorageId: protectRequest.fileStorageId,
correlationId
});
const headers: { [key: string]: string } = {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
};
// Add correlation ID if provided
if (correlationId) {
headers['X-SECLORE-CORRELATION-ID'] = correlationId;
}
const options: IHttpRequestOptions = {
method: 'POST',
url: `${this.baseUrl}/seclore/drm/1.0/protect/fileid`,
headers,
body: protectRequest,
json: true,
};
Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId });
const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true });
Logger.debug(who + 'Protection with file ID successful', {
existingProtectedFileId: protectRequest.existingProtectedFileId,
secloreFileId: (response.body as IProtectWithFileIdResponse).secloreFileId,
correlationId
});
const protectResponse: IProtectWithFileIdResponse = {
...(response.body as IProtectWithFileIdResponse),
headers: response.headers as { [key: string]: string }
};
return protectResponse;
} catch (error: unknown) {
Logger.error(who + 'Protection with file ID failed', {
error,
existingProtectedFileId: protectRequest.existingProtectedFileId,
correlationId
});
this.handleHttpError(error as NodeApiError);
}
}
/**
* 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<IProtectWithHotFolderResponse> - 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<IProtectWithHotFolderResponse> {
const who = "SecloreDRMApiService::protectWithHotFolder:: ";
try {
Logger.debug(who + 'Protecting file with hot folder', {
hotfolderId: protectRequest.hotfolderId,
fileStorageId: protectRequest.fileStorageId,
correlationId
});
const headers: { [key: string]: string } = {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
};
// Add correlation ID if provided
if (correlationId) {
headers['X-SECLORE-CORRELATION-ID'] = correlationId;
}
const options: IHttpRequestOptions = {
method: 'POST',
url: `${this.baseUrl}/seclore/drm/1.0/protect/hf`,
headers,
body: protectRequest,
json: true,
};
Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId });
const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true });
Logger.debug(who + 'Protection with hot folder successful', {
hotfolderId: protectRequest.hotfolderId,
secloreFileId: (response.body as IProtectWithHotFolderResponse).secloreFileId,
correlationId
});
const protectResponse: IProtectWithHotFolderResponse = {
...(response.body as IProtectWithHotFolderResponse),
headers: response.headers as { [key: string]: string }
};
return protectResponse;
} catch (error: unknown) {
Logger.error(who + 'Protection with hot folder failed', {
error,
hotfolderId: protectRequest.hotfolderId,
fileStorageId: protectRequest.fileStorageId,
correlationId
});
this.handleHttpError(error as NodeApiError);
}
}
/**
* 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<IUnprotectResponse> - 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<IUnprotectResponse> {
const who = "SecloreDRMApiService::unprotect:: ";
try {
Logger.debug(who + 'Unprotecting file', {
fileStorageId: unprotectRequest.fileStorageId,
correlationId
});
const headers: { [key: string]: string } = {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
};
// Add correlation ID if provided
if (correlationId) {
headers['X-SECLORE-CORRELATION-ID'] = correlationId;
}
const options: IHttpRequestOptions = {
method: 'POST',
url: `${this.baseUrl}/seclore/drm/1.0/unprotect`,
headers,
body: unprotectRequest,
json: true,
};
Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, correlationId });
const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true });
Logger.debug(who + 'Unprotection successful', {
originalFileStorageId: unprotectRequest.fileStorageId,
unprotectedFileStorageId: (response.body as IUnprotectResponse).fileStorageId,
correlationId
});
const unprotectResponse: IUnprotectResponse = {
...(response.body as IUnprotectResponse),
headers: response.headers as { [key: string]: string }
};
return unprotectResponse;
} catch (error: unknown) {
Logger.error(who + 'Unprotection failed', {
error,
fileStorageId: unprotectRequest.fileStorageId,
correlationId
});
this.handleHttpError(error as NodeApiError);
}
}
/**
* 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<IFileUploadResponse> - 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<IFileUploadResponse> {
const who = "SecloreDRMApiService::uploadFile:: ";
try {
Logger.debug(who + 'Uploading file', {
fileName,
fileSize: fileBuffer.length,
correlationId
});
const headers: { [key: string]: string } = {
Authorization: `Bearer ${accessToken}`,
};
// Add correlation ID if provided
if (correlationId) {
headers['X-SECLORE-CORRELATION-ID'] = correlationId;
}
// 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);
const options: IHttpRequestOptions = {
method: 'POST',
url: `${this.baseUrl}/seclore/drm/filestorage/1.0/upload`,
headers,
body: formData,
nitin.baranwal marked this conversation as resolved
Review

instead of body use formdata which is recommended by n8n for file upload

instead of `body` use `formdata` which is recommended by n8n for file upload
Review

IHttpRequestOptions does not have any field named formdata
the body does accept formdata as input
https://github.com/n8n-io/n8n/blob/master/packages/workflow/src/interfaces.ts#L450

`IHttpRequestOptions` does not have any field named `formdata` the `body` does accept formdata as input https://github.com/n8n-io/n8n/blob/master/packages/workflow/src/interfaces.ts#L450
};
Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, fileName, correlationId });
const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true });
Logger.debug(who + 'File upload successful', {
fileName,
fileStorageId: (response.body as IFileUploadResponse).fileStorageId,
correlationId
});
const uploadResponse: IFileUploadResponse = {
...(response.body as IFileUploadResponse),
headers: response.headers as { [key: string]: string }
};
return uploadResponse;
} catch (error: unknown) {
Logger.error(who + 'File upload failed', {
error,
fileName,
fileSize: fileBuffer.length,
correlationId
});
this.handleHttpError(error as NodeApiError);
}
}
/**
* 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<IFileDownloadResponse> - The downloaded file data with headers
* @throws Error on authentication failure or server error
*/
async downloadFile(
fileStorageId: string,
accessToken: string,
correlationId?: string,
): Promise<IFileDownloadResponse> {
const who = "SecloreDRMApiService::downloadFile:: ";
try {
Logger.debug(who + 'Downloading file', {
fileStorageId,
correlationId
});
const headers: { [key: string]: string } = {
Authorization: `Bearer ${accessToken}`,
};
// Add correlation ID if provided
if (correlationId) {
headers['X-SECLORE-CORRELATION-ID'] = correlationId;
}
const options: IHttpRequestOptions = {
method: 'GET',
url: `${this.baseUrl}/seclore/drm/filestorage/1.0/download/${fileStorageId}`,
headers,
encoding: 'arraybuffer',
};
Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, fileStorageId, correlationId });
const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true });
const fileData = new Uint8Array(response.body as ArrayBuffer);
Logger.debug(who + 'File download successful', {
fileStorageId,
fileSize: fileData.length,
correlationId
});
const downloadResponse: IFileDownloadResponse = {
data: fileData,
headers: response.headers as { [key: string]: string }
};
return downloadResponse;
} catch (error: unknown) {
Logger.error(who + 'File download failed', {
error,
fileStorageId,
correlationId
});
this.handleHttpError(error as NodeApiError);
}
}
/**
* Deletes a file with fileStorageId from file storage of currently logged in Tenant.
*
* @param fileStorageId - Storage ID of the file to be deleted
* @param accessToken - JWT access token for authorization
* @param correlationId - Optional request ID for logging purpose
* @returns Promise<IFileDeleteResponse> - Response headers on successful deletion
* @throws Error on authentication failure or server error
*/
async deleteFile(
fileStorageId: string,
accessToken: string,
correlationId?: string,
): Promise<IFileDeleteResponse> {
const who = "SecloreDRMApiService::deleteFile:: ";
try {
Logger.debug(who + 'Deleting file', {
fileStorageId,
correlationId
});
const headers: { [key: string]: string } = {
Authorization: `Bearer ${accessToken}`,
};
// Add correlation ID if provided
if (correlationId) {
headers['X-SECLORE-CORRELATION-ID'] = correlationId;
}
const options: IHttpRequestOptions = {
method: 'DELETE',
url: `${this.baseUrl}/seclore/drm/filestorage/1.0/${fileStorageId}`,
headers,
};
Logger.debug(who + 'Making HTTP request', { url: options.url, method: options.method, fileStorageId, correlationId });
const response = await this.context.helpers.httpRequest({ ...options, returnFullResponse: true });
Logger.debug(who + 'File deletion successful', {
fileStorageId,
correlationId
});
const deleteResponse: IFileDeleteResponse = {
headers: response.headers as { [key: string]: string }
};
return deleteResponse;
} catch (error: unknown) {
Logger.error(who + 'File deletion failed', {
error,
fileStorageId,
correlationId
});
this.handleHttpError(error as NodeApiError);
}
}
}

View File

@ -0,0 +1,388 @@
import { IExecuteFunctions, LoggerProxy as Logger } from 'n8n-workflow';
import { IFileDownloadResponse, 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<void>;
private loginPromise?: Promise<void>;
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<void> {
// If we don't have a token or it's expired, login
if (!this.accessToken || (this.tokenExpiry && new Date() >= this.tokenExpiry)) {
// If there's already a login in progress, wait for it
if (this.loginPromise) {
await this.loginPromise;
return;
}
// Start login and store the promise
this.loginPromise = this.login(correlationId);
try {
await this.loginPromise;
} finally {
this.loginPromise = undefined;
}
}
}
/**
* Performs login and stores tokens
* @param correlationId - Optional correlation ID for logging
*/
private async login(correlationId?: string): Promise<void> {
const who = "SecloreDRMFileService::login:: ";
try {
Logger.debug(who + 'Attempting login', { tenantId: this.tenantId, correlationId });
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);
Logger.info(who + 'Login successful', { tenantId: this.tenantId, correlationId });
} catch (error) {
Logger.error(who + 'Login failed', { error, tenantId: this.tenantId, correlationId });
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<void> {
// If there's already a refresh in progress, wait for it
if (this.refreshPromise) {
await this.refreshPromise;
return;
}
if (!this.refreshToken) {
throw new Error('No refresh token available');
}
// Start refresh and store the promise
this.refreshPromise = this.performTokenRefresh(correlationId);
try {
await this.refreshPromise;
} finally {
this.refreshPromise = undefined;
}
}
/**
* Performs the actual token refresh
* @param correlationId - Optional correlation ID for logging
*/
private async performTokenRefresh(correlationId?: string): Promise<void> {
const who = "SecloreDRMFileService::performTokenRefresh:: ";
try {
Logger.debug(who + 'Attempting token refresh', { correlationId });
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);
Logger.info(who + 'Token refresh successful', { correlationId });
} catch (error) {
Logger.error(who + 'Token refresh failed', { error, correlationId });
this.clearTokens();
throw error;
}
}
/**
* Clears stored tokens and pending promises
*/
private clearTokens(): void {
this.accessToken = undefined;
this.refreshToken = undefined;
this.tokenExpiry = undefined;
this.refreshPromise = undefined;
this.loginPromise = undefined;
}
/**
* Executes an API call with automatic authentication and retry logic
* @param apiCall - The API call function to execute
* @param retryCount - Number of retries (defaults to class default)
* @param correlationId - Optional correlation ID for logging
*/
private async executeWithRetry<T>(
apiCall: (accessToken: string) => Promise<T>,
retryCount: number = this.defaultRetryCount,
correlationId?: string,
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt <= retryCount; attempt++) {
try {
// Ensure we have a valid token
await this.ensureAuthenticated(correlationId);
if (!this.accessToken) {
throw new Error('Failed to obtain access token');
}
// Execute the API call
return await apiCall(this.accessToken);
} catch (error: 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 (refreshError: unknown) {
Logger.error('SecloreDRMFileService::executeWithRetry:: Token refresh failed', { refreshError, correlationId });
// 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) => {
nitin.baranwal marked this conversation as resolved
Review

Instead of this code block, use:

const delayMs = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delayMs));

Instead of this code block, use: const delayMs = Math.pow(2, attempt) * 1000; await new Promise((resolve) => setTimeout(resolve, delayMs));
Review
n8n-nodes-seclore\nodes\SecloreProtect\Services\SecloreDRMFileService.ts
  189:36  error  Use of restricted global 'setTimeout' is not allowed  @n8n/community-nodes/no-restricted-globals

lint fails if setTmeout is used

``` n8n-nodes-seclore\nodes\SecloreProtect\Services\SecloreDRMFileService.ts 189:36 error Use of restricted global 'setTimeout' is not allowed @n8n/community-nodes/no-restricted-globals ``` lint fails if setTmeout is used
const delay = Math.pow(2, attempt) * 1000;
// Use a simple delay implementation
const start = Date.now();
while (Date.now() - start < delay) {
// Busy wait for delay
}
resolve(undefined);
});
}
}
throw lastError!;
}
/**
* Protect file using external identifier with automatic authentication and retry
*/
async protectWithExternalRefId(
protectRequest: IProtectWithExternalRefIdRequest,
correlationId?: string,
retryCount?: number,
): Promise<IProtectWithExternalRefIdResponse> {
const who = "SecloreDRMFileService::protectWithExternalRefId:: ";
try {
Logger.debug(who + 'Protecting file with external ref ID', { fileStorageId: protectRequest.fileStorageId, hotfolderExternalReferenceId: protectRequest.hotfolderExternalReference.externalReferenceId, correlationId });
const result = await this.executeWithRetry(
(accessToken) =>
this.apiService.protectWithExternalRefId(protectRequest, accessToken, correlationId),
retryCount,
correlationId,
);
Logger.info(who + 'File protected with external ref ID successfully', { fileStorageId: protectRequest.fileStorageId, secloreFileId: result.secloreFileId, correlationId });
return result;
} catch (error) {
Logger.error(who + 'Protect with external ref ID failed', { error, fileStorageId: protectRequest.fileStorageId, correlationId });
throw error;
}
}
/**
* Protect file using existing protected file ID with automatic authentication and retry
*/
async protectWithFileId(
protectRequest: IProtectWithFileIdRequest,
correlationId?: string,
retryCount?: number,
): Promise<IProtectWithFileIdResponse> {
const who = "SecloreDRMFileService::protectWithFileId:: ";
try {
Logger.debug(who + 'Protecting file with file ID', { existingProtectedFileId: protectRequest.existingProtectedFileId, fileStorageId: protectRequest.fileStorageId, correlationId });
const result = await this.executeWithRetry(
(accessToken) =>
this.apiService.protectWithFileId(protectRequest, accessToken, correlationId),
retryCount,
correlationId,
);
Logger.info(who + 'File protected with file ID successfully', { existingProtectedFileId: protectRequest.existingProtectedFileId, secloreFileId: result.secloreFileId, correlationId });
return result;
} catch (error) {
Logger.error(who + 'Protect with file ID failed', { error, existingProtectedFileId: protectRequest.existingProtectedFileId, correlationId });
throw error;
}
}
/**
* Protect file using HotFolder ID with automatic authentication and retry
*/
async protectWithHotFolder(
protectRequest: IProtectWithHotFolderRequest,
correlationId?: string,
retryCount?: number,
): Promise<IProtectWithHotFolderResponse> {
const who = "SecloreDRMFileService::protectWithHotFolder:: ";
try {
Logger.debug(who + 'Protecting file with hot folder', { hotfolderId: protectRequest.hotfolderId, fileStorageId: protectRequest.fileStorageId, correlationId });
const result = await this.executeWithRetry(
(accessToken) =>
this.apiService.protectWithHotFolder(protectRequest, accessToken, correlationId),
retryCount,
correlationId,
);
Logger.info(who + 'File protected with hot folder successfully', { hotfolderId: protectRequest.hotfolderId, fileStorageId: protectRequest.fileStorageId, secloreFileId: result.secloreFileId, correlationId });
return result;
} catch (error) {
Logger.error(who + 'Protect with hot folder failed', { error, hotfolderId: protectRequest.hotfolderId, fileStorageId: protectRequest.fileStorageId, correlationId });
throw error;
}
}
/**
* Unprotect file with automatic authentication and retry
*/
async unprotect(
unprotectRequest: IUnprotectRequest,
correlationId?: string,
retryCount?: number,
): Promise<IUnprotectResponse> {
const who = "SecloreDRMFileService::unprotect:: ";
try {
Logger.debug(who + 'Unprotecting file', { fileStorageId: unprotectRequest.fileStorageId, correlationId });
const result = await this.executeWithRetry(
(accessToken) => this.apiService.unprotect(unprotectRequest, accessToken, correlationId),
retryCount,
correlationId,
);
Logger.info(who + 'File unprotected successfully', { originalFileStorageId: unprotectRequest.fileStorageId, unprotectedFileStorageId: result.fileStorageId, correlationId });
return result;
} catch (error) {
Logger.error(who + 'Unprotect file failed', { error, fileStorageId: unprotectRequest.fileStorageId, correlationId });
throw error;
}
}
/**
* Upload file with automatic authentication and retry
*/
async uploadFile(
fileBuffer: Uint8Array,
fileName: string,
correlationId?: string,
retryCount?: number,
): Promise<IFileUploadResponse> {
const who = "SecloreDRMFileService::uploadFile:: ";
try {
Logger.debug(who + 'Uploading file', { fileName, correlationId });
const result = await this.executeWithRetry(
(accessToken) => this.apiService.uploadFile(fileBuffer, fileName, accessToken, correlationId),
retryCount,
correlationId,
);
Logger.info(who + 'File uploaded successfully', { fileName, correlationId });
return result;
} catch (error) {
Logger.error(who + 'Upload file failed', { error, fileName, correlationId });
throw error;
}
}
/**
* 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<IFileDownloadResponse> {
const who = "SecloreDRMFileService::downloadFile:: ";
try {
Logger.debug(who + 'Downloading file', { fileStorageId, correlationId });
const result = await this.executeWithRetry(
(accessToken) => this.apiService.downloadFile(fileStorageId, accessToken, correlationId),
retryCount,
correlationId,
);
Logger.info(who + 'File downloaded successfully', { fileStorageId, fileSize: result.data.length, correlationId });
return result;
} catch (error) {
Logger.error(who + 'Download file failed', { error, fileStorageId, correlationId });
throw error;
}
}
/**
* Delete file with automatic authentication and retry
*/
async deleteFile(
fileStorageId: string,
correlationId?: string,
retryCount?: number,
): Promise<void> {
const who = "SecloreDRMFileService::deleteFile:: ";
try {
Logger.debug(who + 'Deleting file', { fileStorageId, correlationId });
await this.executeWithRetry(
(accessToken) => this.apiService.deleteFile(fileStorageId, accessToken, correlationId),
retryCount,
correlationId,
);
Logger.info(who + 'File deleted successfully', { fileStorageId, correlationId });
} catch (error) {
Logger.error(who + 'Delete file failed', { error, fileStorageId, correlationId });
throw error;
}
}
/**
* 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);
}
}

View File

@ -0,0 +1,60 @@
import { LoggerProxy as Logger } from 'n8n-workflow';
/**
* Extracts filename from Content-Disposition header
* @param headers - Response headers object
* @returns The extracted filename or null if not found
*/
export function getFileNameFromHeaders(headers?: { [key: string]: string }): string | null {
const who = "Utils::getFileNameFromHeaders:: ";
if (!headers) {
Logger.debug(who + 'No headers provided');
return null;
}
// Look for content-disposition header (case-insensitive)
const contentDisposition = Object.keys(headers).find(key =>
key.toLowerCase() === 'content-disposition'
);
if (!contentDisposition || !headers[contentDisposition]) {
Logger.debug(who + 'Content-Disposition header not found');
return null;
}
const headerValue = headers[contentDisposition];
Logger.debug(who + 'Found Content-Disposition header', { headerValue });
// Handle different filename formats in Content-Disposition header
// Format 1: filename*=UTF-8''encoded-filename
const utf8Match = headerValue.match(/filename\*=UTF-8''([^;]+)/i);
if (utf8Match) {
try {
const decodedFilename = decodeURIComponent(utf8Match[1]);
Logger.debug(who + 'Extracted filename from UTF-8 format', { filename: decodedFilename });
return decodedFilename;
} catch (error) {
Logger.error(who + 'Failed to decode UTF-8 filename', { error, encodedFilename: utf8Match[1] });
}
}
// Format 2: filename="quoted-filename"
const quotedMatch = headerValue.match(/filename="([^"]+)"/i);
if (quotedMatch) {
Logger.debug(who + 'Extracted filename from quoted format', { filename: quotedMatch[1] });
return quotedMatch[1];
}
// Format 3: filename=unquoted-filename
const unquotedMatch = headerValue.match(/filename=([^;]+)/i);
if (unquotedMatch) {
const filename = unquotedMatch[1].trim();
Logger.debug(who + 'Extracted filename from unquoted format', { filename });
return filename;
}
Logger.debug(who + 'Could not extract filename from Content-Disposition header', { headerValue });
return null;
}

View File

@ -0,0 +1,314 @@
import {
IExecuteFunctions,
INodeExecutionData,
NodeOperationError,
NodeOutput,
LoggerProxy as Logger,
} from 'n8n-workflow';
import crypto from 'node:crypto';
import { SecloreDRMFileService } from '../Services/SecloreDRMFileService';
import { getFileNameFromHeaders } from '../Services/Utils';
/**
* Deletes a file from storage with error handling (does not throw errors)
* @param fileService - The SecloreDRMFileService instance
* @param fileStorageId - The file storage ID to delete
* @param correlationId - Optional correlation ID for tracking
* @param retryCount - Number of retries for operations
*/
async function deleteFile(
fileService: SecloreDRMFileService,
fileStorageId: string,
correlationId?: string,
retryCount: number = 3,
): Promise<void> {
const who = "protectWithHotFolder::deleteFile:: ";
try {
Logger.debug(who + 'Attempting to delete file', { fileStorageId, correlationId, retryCount });
await fileService.deleteFile(
fileStorageId,
correlationId,
retryCount,
);
Logger.debug(who + 'File deleted successfully', { fileStorageId, correlationId });
} catch (error) {
// Log error but don't throw - this is for cleanup operations
Logger.error(who + 'File deletion failed, continuing operation', {
error,
fileStorageId,
correlationId,
message: 'This is a cleanup operation, continuing despite deletion failure'
});
}
}
/**
* Uploads a file, protects it with hot folder, and downloads the protected version
* @param fileService - The SecloreDRMFileService instance
* @param fileBuffer - The file buffer to upload
* @param fileName - The name of the file
* @param hotfolderId - The hot folder ID to use for protection
* @param correlationId - Optional correlation ID for tracking
* @param retryCount - Number of retries for operations
* @returns Promise containing the protected file data and metadata
*/
async function protectFileWithHotFolder(
fileService: SecloreDRMFileService,
fileBuffer: Buffer,
fileName: string,
hotfolderId: string,
correlationId?: string,
retryCount: number = 3,
): Promise<{
protectedFileData: Uint8Array;
originalFileStorageId: string;
protectedFileStorageId: string;
secloreFileId: string;
fileName: string;
fileSize: number;
}> {
const who = "protectWithHotFolder::protectFileWithHotFolder:: ";
let originalFileStorageId: string = '';
try {
Logger.debug(who + 'Starting protect file with hot folder operation', { fileName, fileSize: fileBuffer.length, hotfolderId, correlationId, retryCount });
// Upload the file first
Logger.debug(who + 'Uploading file', { fileName, fileSize: fileBuffer.length, correlationId });
const uploadResult = await fileService.uploadFile(
new Uint8Array(fileBuffer),
fileName,
correlationId,
retryCount,
);
Logger.debug(who + 'File uploaded successfully', { fileStorageId: uploadResult.fileStorageId, fileName, correlationId });
// check if the file is already protected
if (uploadResult.protected) {
throw new Error('File is already protected');
}
originalFileStorageId = uploadResult.fileStorageId;
// Protect the uploaded file with HotFolder
Logger.debug(who + 'Protecting file with hot folder', { fileStorageId: uploadResult.fileStorageId, hotfolderId, fileName, correlationId });
const protectResult = await fileService.protectWithHotFolder(
{
hotfolderId,
fileStorageId: uploadResult.fileStorageId,
},
correlationId,
retryCount,
);
Logger.debug(who + 'File protected successfully', {
originalFileStorageId: uploadResult.fileStorageId,
protectedFileStorageId: protectResult.fileStorageId,
secloreFileId: protectResult.secloreFileId,
hotfolderId,
fileName,
correlationId
});
// Download the protected file
Logger.debug(who + 'Downloading protected file', { fileStorageId: protectResult.fileStorageId, fileName, correlationId });
const protectedFileData = await fileService.downloadFile(
protectResult.fileStorageId,
correlationId,
retryCount,
);
// Try to get the actual filename from response headers, fallback to constructed name
const actualFileName = getFileNameFromHeaders(protectedFileData.headers) || fileName;
Logger.debug(who + 'Protected file downloaded successfully', {
fileStorageId: protectResult.fileStorageId,
fileSize: protectedFileData.data.length,
originalFileName: fileName,
actualFileName,
correlationId
});
const result = {
protectedFileData: protectedFileData.data,
originalFileStorageId: uploadResult.fileStorageId,
protectedFileStorageId: protectResult.fileStorageId,
secloreFileId: protectResult.secloreFileId,
fileName: actualFileName,
fileSize: protectedFileData.data.length,
};
Logger.debug(who + 'Protect file with hot folder operation completed successfully', {
fileName: result.fileName,
originalFileStorageId: result.originalFileStorageId,
protectedFileStorageId: result.protectedFileStorageId,
secloreFileId: result.secloreFileId,
fileSize: result.fileSize,
hotfolderId,
correlationId
});
return result;
} catch (error) {
Logger.error(who + 'Protect file with hot folder operation failed', { error, fileName, hotfolderId, correlationId });
throw error;
} finally {
if (originalFileStorageId !== '') {
await deleteFile(fileService, originalFileStorageId, correlationId, retryCount);
}
}
}
export async function protectWithHotFolder(this: IExecuteFunctions): Promise<NodeOutput> {
const who = "protectWithHotFolder::protectWithHotFolder:: ";
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
// Initialize logger with the current execution context
Logger.init(this.logger);
Logger.debug(who + 'Seclore Protect with HotFolder operation started', { itemCount: items.length });
// Get credentials
Logger.debug(who + 'Getting credentials', {});
const credentials = await this.getCredentials('secloreProtectApi');
const baseUrl = credentials.baseUrl as string;
const tenantId = credentials.tenantId as string;
const tenantSecret = credentials.tenantSecret as string;
// Initialize the file service
Logger.debug(who + 'Initializing file service', { baseUrl, tenantId });
const fileService = new SecloreDRMFileService(this, baseUrl, tenantId, tenantSecret);
for (let i = 0; i < items.length; i++) {
Logger.debug(who + 'Processing item', { itemIndex: i });
try {
// Get parameters for this item
Logger.debug(who + 'Getting node parameters', { itemIndex: i });
const hotfolderId = this.getNodeParameter('hotfolderId', i) as string;
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string;
const correlationId = crypto.randomUUID();
const retryCount = this.getNodeParameter('retryCount', i) as number;
// Validate required parameters
if (!hotfolderId) {
throw new NodeOperationError(this.getNode(), 'HotFolder ID is required', {
itemIndex: i,
});
}
Logger.debug(who + 'Asserting binary data', { binaryPropertyName, itemIndex: i });
// Get input binary data
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
Logger.debug(who + 'Getting binary data buffer', { binaryPropertyName, itemIndex: i });
const fileBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
Logger.debug(who + 'Binary data retrieved', {
fileName: binaryData.fileName,
fileSize: fileBuffer.length,
mimeType: binaryData.mimeType,
itemIndex: i,
correlationId,
retryCount
});
// Use the combined upload, protect, and download function
try {
Logger.debug(who + 'Starting protect file with hot folder operation', {
fileName: binaryData.fileName,
hotfolderId,
correlationId,
retryCount,
itemIndex: i
});
const result = await protectFileWithHotFolder(
fileService,
fileBuffer,
binaryData.fileName || 'file',
hotfolderId,
correlationId,
retryCount,
);
Logger.debug(who + 'Protect file with hot folder operation completed successfully', {
fileName: result.fileName,
originalFileStorageId: result.originalFileStorageId,
protectedFileStorageId: result.protectedFileStorageId,
secloreFileId: result.secloreFileId,
fileSize: result.fileSize,
hotfolderId,
itemIndex: i,
correlationId
});
// Create output binary data
Logger.debug(who + 'Preparing binary data for output', {
fileName: result.fileName,
mimeType: binaryData.mimeType,
fileSize: result.fileSize,
itemIndex: i
});
const outputBinaryData = await this.helpers.prepareBinaryData(
Buffer.from(result.protectedFileData),
result.fileName,
binaryData.mimeType,
);
// Create return item with binary data and metadata
const returnItem: INodeExecutionData = {
json: {
success: true,
originalFileStorageId: result.originalFileStorageId,
protectedFileStorageId: result.protectedFileStorageId,
secloreFileId: result.secloreFileId,
hotfolderId,
fileName: result.fileName,
fileSize: result.fileSize,
correlationId: correlationId,
},
binary: {
data: outputBinaryData,
},
};
Logger.debug(who + 'Adding result to return data', { itemIndex: i, success: true });
returnData.push(returnItem);
} catch (protectError) {
Logger.error(who + 'Protect file with hot folder operation failed', { protectError, itemIndex: i });
// Re-throw the error to be handled by the outer catch block
throw protectError;
}
} catch (error) {
// Handle errors gracefully
Logger.error(who + 'Item processing failed', { error, itemIndex: i });
if (this.continueOnFail()) {
Logger.debug(who + 'Continuing on fail, adding error item', { itemIndex: i, errorMessage: error.message });
const returnItem: INodeExecutionData = {
json: {
success: false,
error: error.message,
itemIndex: i,
},
};
returnData.push(returnItem);
} else {
Logger.error(who + 'Throwing NodeOperationError', { error: error.message, itemIndex: i });
throw new NodeOperationError(this.getNode(), error.message, {
itemIndex: i,
});
}
}
}
Logger.debug(who + 'Seclore Protect with HotFolder operation completed', {
processedItems: returnData.length,
successfulItems: returnData.filter(item => item.json.success).length
});
return [returnData];
}

View File

@ -0,0 +1,298 @@
import {
IExecuteFunctions,
INodeExecutionData,
NodeOperationError,
NodeOutput,
LoggerProxy as Logger,
} from 'n8n-workflow';
import crypto from 'node:crypto';
import { SecloreDRMFileService } from '../Services/SecloreDRMFileService';
import { getFileNameFromHeaders } from '../Services/Utils';
/**
* Deletes a file from storage with error handling (does not throw errors)
* @param fileService - The SecloreDRMFileService instance
* @param fileStorageId - The file storage ID to delete
* @param correlationId - Optional correlation ID for tracking
* @param retryCount - Number of retries for operations
*/
async function deleteFile(
fileService: SecloreDRMFileService,
fileStorageId: string,
correlationId?: string,
retryCount: number = 3,
): Promise<void> {
const who = "unprotect::deleteFileWithErrorHandling:: ";
try {
Logger.debug(who + 'Attempting to delete file', { fileStorageId, correlationId, retryCount });
await fileService.deleteFile(
fileStorageId,
correlationId,
retryCount,
);
Logger.debug(who + 'File deleted successfully', { fileStorageId, correlationId });
} catch (error) {
// Log error but don't throw - this is for cleanup operations
Logger.error(who + 'File deletion failed, continuing operation', {
error,
fileStorageId,
correlationId,
message: 'This is a cleanup operation, continuing despite deletion failure'
});
}
}
/**
* Uploads a file, unprotects it, and downloads the unprotected version
* @param fileService - The SecloreDRMFileService instance
* @param fileBuffer - The file buffer to upload
* @param fileName - The name of the file
* @param correlationId - Optional correlation ID for tracking
* @param retryCount - Number of retries for operations
* @returns Promise containing the unprotected file data and metadata
*/
async function unprotectFile(
fileService: SecloreDRMFileService,
fileBuffer: Buffer,
fileName: string,
correlationId?: string,
retryCount: number = 3,
): Promise<{
unprotectedFileData: Uint8Array;
originalFileStorageId: string;
unprotectedFileStorageId: string;
fileName: string;
fileSize: number;
}> {
const who = "unprotect::unprotectFile:: ";
let originalFileStorageId: string = '';
try {
Logger.debug(who + 'Starting unprotect file operation', { fileName, fileSize: fileBuffer.length, correlationId, retryCount });
// Upload the protected file
Logger.debug(who + 'Uploading protected file', { fileName, fileSize: fileBuffer.length, correlationId });
const uploadResult = await fileService.uploadFile(
new Uint8Array(fileBuffer),
fileName,
correlationId,
retryCount,
);
Logger.debug(who + 'File uploaded successfully', { fileStorageId: uploadResult.fileStorageId, fileName, correlationId });
originalFileStorageId = uploadResult.fileStorageId;
// check if the file is already unprotected
if (!uploadResult.protected) {
Logger.debug(who + 'File is already unprotected', { fileStorageId: uploadResult.fileStorageId, fileName, correlationId });
return {
unprotectedFileData: fileBuffer,
originalFileStorageId: uploadResult.fileStorageId,
unprotectedFileStorageId: uploadResult.fileStorageId,
fileName,
fileSize: fileBuffer.length,
};
}
// Unprotect the uploaded file
Logger.debug(who + 'Unprotecting file', { fileStorageId: uploadResult.fileStorageId, fileName, correlationId });
const unprotectResult = await fileService.unprotect(
{
fileStorageId: uploadResult.fileStorageId,
},
correlationId,
retryCount,
);
Logger.debug(who + 'File unprotected successfully', {
originalFileStorageId: uploadResult.fileStorageId,
unprotectedFileStorageId: unprotectResult.fileStorageId,
fileName,
correlationId
});
// Download the unprotected file
Logger.debug(who + 'Downloading unprotected file', { fileStorageId: unprotectResult.fileStorageId, fileName, correlationId });
const unprotectedFileData = await fileService.downloadFile(
unprotectResult.fileStorageId,
correlationId,
retryCount,
);
// Try to get the actual filename from response headers, fallback to original filename
const actualFileName = getFileNameFromHeaders(unprotectedFileData.headers) || fileName;
Logger.debug(who + 'Unprotected file downloaded successfully', {
fileStorageId: unprotectResult.fileStorageId,
fileSize: unprotectedFileData.data.length,
originalFileName: fileName,
actualFileName: actualFileName,
correlationId
});
const result = {
unprotectedFileData: unprotectedFileData.data,
originalFileStorageId: uploadResult.fileStorageId,
unprotectedFileStorageId: unprotectResult.fileStorageId,
fileName: actualFileName,
fileSize: unprotectedFileData.data.length,
};
Logger.debug(who + 'Unprotect file operation completed successfully', {
fileName: result.fileName,
originalFileStorageId: result.originalFileStorageId,
unprotectedFileStorageId: result.unprotectedFileStorageId,
fileSize: result.fileSize,
correlationId
});
return result;
} catch (error) {
Logger.error(who + 'Unprotect file operation failed', { error, fileName, correlationId });
throw error;
} finally {
if (originalFileStorageId !== '') {
await deleteFile(fileService, originalFileStorageId, correlationId, retryCount);
}
}
}
export async function unprotect(this: IExecuteFunctions): Promise<NodeOutput> {
const who = "unprotect::unprotect:: ";
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
// Initialize logger with the current execution context
Logger.init(this.logger);
Logger.debug(who + 'Seclore Unprotect operation started', { itemCount: items.length });
// Get credentials
Logger.debug(who + 'Getting credentials', {});
const credentials = await this.getCredentials('secloreProtectApi');
const baseUrl = credentials.baseUrl as string;
const tenantId = credentials.tenantId as string;
const tenantSecret = credentials.tenantSecret as string;
// Initialize the file service
Logger.debug(who + 'Initializing file service', { baseUrl, tenantId });
const fileService = new SecloreDRMFileService(this, baseUrl, tenantId, tenantSecret);
for (let i = 0; i < items.length; i++) {
Logger.debug(who + 'Processing item', { itemIndex: i });
try {
// Get parameters for this item
Logger.debug(who + 'Getting node parameters', { itemIndex: i });
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string;
const correlationId = crypto.randomUUID();
const retryCount = this.getNodeParameter('retryCount', i) as number;
Logger.debug(who + 'Asserting binary data', { binaryPropertyName, itemIndex: i });
// Get input binary data
const binaryData = this.helpers.assertBinaryData(i, binaryPropertyName);
Logger.debug(who + 'Getting binary data buffer', { binaryPropertyName, itemIndex: i });
const fileBuffer = await this.helpers.getBinaryDataBuffer(i, binaryPropertyName);
Logger.debug(who + 'Binary data retrieved', {
fileName: binaryData.fileName,
fileSize: fileBuffer.length,
mimeType: binaryData.mimeType,
itemIndex: i,
correlationId,
retryCount
});
// Use the combined upload, unprotect, and download function
try {
Logger.debug(who + 'Starting unprotect file operation', {
fileName: binaryData.fileName,
correlationId,
retryCount,
itemIndex: i
});
const result = await unprotectFile(
fileService,
fileBuffer,
binaryData.fileName || 'protected_file',
correlationId,
retryCount,
);
Logger.debug(who + 'Unprotect file operation completed successfully', {
fileName: result.fileName,
originalFileStorageId: result.originalFileStorageId,
unprotectedFileStorageId: result.unprotectedFileStorageId,
fileSize: result.fileSize,
itemIndex: i,
correlationId
});
// Create output binary data
Logger.debug(who + 'Preparing binary data for output', {
fileName: binaryData.fileName,
mimeType: binaryData.mimeType,
fileSize: result.fileSize,
itemIndex: i
});
const outputBinaryData = await this.helpers.prepareBinaryData(
Buffer.from(result.unprotectedFileData),
result.fileName,
binaryData.mimeType,
);
// Create return item with binary data and metadata
const returnItem: INodeExecutionData = {
json: {
success: true,
originalFileStorageId: result.originalFileStorageId,
unprotectedFileStorageId: result.unprotectedFileStorageId,
fileName: result.fileName,
fileSize: result.fileSize,
correlationId: correlationId,
},
binary: {
data: outputBinaryData,
},
};
Logger.debug(who + 'Adding result to return data', { itemIndex: i, success: true });
returnData.push(returnItem);
} catch (unprotectError) {
Logger.error(who + 'Unprotect file operation failed', { unprotectError, itemIndex: i });
// Re-throw the error to be handled by the outer catch block
throw unprotectError;
}
} catch (error) {
// Handle errors gracefully
Logger.error(who + 'Item processing failed', { error, itemIndex: i });
if (this.continueOnFail()) {
Logger.debug(who + 'Continuing on fail, adding error item', { itemIndex: i, errorMessage: error.message });
const returnItem: INodeExecutionData = {
json: {
success: false,
error: error.message,
itemIndex: i,
},
};
returnData.push(returnItem);
} else {
Logger.error(who + 'Throwing NodeOperationError', { error: error.message, itemIndex: i });
throw new NodeOperationError(this.getNode(), error.message, {
itemIndex: i,
});
}
}
}
Logger.debug(who + 'Seclore Unprotect operation completed', {
processedItems: returnData.length,
successfulItems: returnData.filter(item => item.json.success).length
});
return [returnData];
}

51
package.json Normal file
View File

@ -0,0 +1,51 @@
{
"name": "@seclore/n8n-nodes-seclore",
"version": "0.1.0",
"description": "n8n community node for Seclore Protect - secure file protection using DRM technology",
"license": "MIT",
"homepage": "",
"keywords": [
"n8n-community-node-package"
],
"author": {
"name": "Seclore Technology",
"email": "support@seclore.com"
},
"repository": {
"type": "git",
"url": "https://repo.seclore.com/public/n8n-nodes-seclore.git"
},
"scripts": {
"build": "n8n-node build",
"build:watch": "tsc --watch",
"dev": "n8n-node dev",
"lint": "n8n-node lint",
"lint:fix": "n8n-node lint --fix",
"release": "n8n-node release",
"prepublishOnly": "n8n-node prerelease"
},
"files": [
"dist"
],
"n8n": {
"n8nNodesApiVersion": 1,
"strict": true,
"credentials": [
"dist/credentials/SecloreProtectApi.credentials.js"
],
"nodes": [
"dist/nodes/SecloreProtect/SecloreProtect.node.js"
]
},
"devDependencies": {
"@n8n/node-cli": "*",
"@types/node": "^24.9.1",
"eslint": "9.32.0",
"prettier": "3.6.2",
"release-it": "^19.0.4",
"typescript": "5.9.2"
},
"peerDependencies": {
"n8n-workflow": "*"
}
}

26
tsconfig.json Normal file
View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"strict": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2019",
"lib": ["es2019", "es2020", "es2022.error"],
"removeComments": true,
"useUnknownInCatchVariables": false,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"preserveConstEnums": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"incremental": true,
"declaration": true,
"sourceMap": true,
"skipLibCheck": true,
"outDir": "./dist/",
"types": ["node"]
},
"include": ["credentials/**/*", "nodes/**/*", "nodes/**/*.json", "package.json"]
}