Credentials, functionality

This commit is contained in:
Rupenieks 2020-06-03 16:24:17 +02:00
parent 4684e32375
commit 263ebb0fc1
4 changed files with 296 additions and 237 deletions

View file

@ -0,0 +1,54 @@
import {
ICredentialType,
NodePropertyTypes,
} from 'n8n-workflow';
export class DropboxOAuth2Api implements ICredentialType {
name = 'dropboxOAuth2Api';
extends = [
'oAuth2Api',
];
displayName = 'Dropbox OAuth2 API';
properties = [
{
displayName: 'Dropbox Server',
name: 'server',
type: 'string' as NodePropertyTypes,
default: 'https://api.dropboxapi.com',
description: 'The server to connect to. Does only have to get changed if Dropbox Enterprise gets used.',
},
{
displayName: 'Authorization URL',
name: 'authUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://www.dropbox.com/oauth2/authorize',
required: true,
},
{
displayName: 'Access Token URL',
name: 'accessTokenUrl',
type: 'hidden' as NodePropertyTypes,
default: 'https://api.dropboxapi.com/oauth2/token',
required: true,
},
{
displayName: 'Scope',
name: 'scope',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Auth URI Query Parameters',
name: 'authQueryParameters',
type: 'hidden' as NodePropertyTypes,
default: '',
},
{
displayName: 'Authentication',
name: 'authentication',
type: 'hidden' as NodePropertyTypes,
default: 'header',
},
];
}

View file

@ -1,16 +1,11 @@
import { import { BINARY_ENCODING, IExecuteFunctions } from 'n8n-core';
BINARY_ENCODING,
IExecuteFunctions,
} from 'n8n-core';
import { import {
IDataObject, IDataObject,
INodeTypeDescription,
INodeExecutionData, INodeExecutionData,
INodeType, INodeType,
INodeTypeDescription
} from 'n8n-workflow'; } from 'n8n-workflow';
import { dropboxApiRequest } from './GenericFunctions';
import { OptionsWithUri } from 'request';
export class Dropbox implements INodeType { export class Dropbox implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
@ -23,7 +18,7 @@ export class Dropbox implements INodeType {
description: 'Access data on Dropbox', description: 'Access data on Dropbox',
defaults: { defaults: {
name: 'Dropbox', name: 'Dropbox',
color: '#22BB44', color: '#22BB44'
}, },
inputs: ['main'], inputs: ['main'],
outputs: ['main'], outputs: ['main'],
@ -31,9 +26,40 @@ export class Dropbox implements INodeType {
{ {
name: 'dropboxApi', name: 'dropboxApi',
required: true, required: true,
displayOptions: {
show: {
authentication: ['accessToken']
}
}
},
{
name: 'dropboxOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth2']
}
}
} }
], ],
properties: [ properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
name: 'Access Token',
value: 'accessToken'
},
{
name: 'OAuth2',
value: 'oAuth2'
}
],
default: 'accessToken',
description: 'Means of authenticating with the serivce.'
},
{ {
displayName: 'Resource', displayName: 'Resource',
name: 'resource', name: 'resource',
@ -41,15 +67,15 @@ export class Dropbox implements INodeType {
options: [ options: [
{ {
name: 'File', name: 'File',
value: 'file', value: 'file'
}, },
{ {
name: 'Folder', name: 'Folder',
value: 'folder', value: 'folder'
}, }
], ],
default: 'file', default: 'file',
description: 'The resource to operate on.', description: 'The resource to operate on.'
}, },
// ---------------------------------- // ----------------------------------
@ -61,40 +87,38 @@ export class Dropbox implements INodeType {
type: 'options', type: 'options',
displayOptions: { displayOptions: {
show: { show: {
resource: [ resource: ['file']
'file', }
],
},
}, },
options: [ options: [
{ {
name: 'Copy', name: 'Copy',
value: 'copy', value: 'copy',
description: 'Copy a file', description: 'Copy a file'
}, },
{ {
name: 'Delete', name: 'Delete',
value: 'delete', value: 'delete',
description: 'Delete a file', description: 'Delete a file'
}, },
{ {
name: 'Download', name: 'Download',
value: 'download', value: 'download',
description: 'Download a file', description: 'Download a file'
}, },
{ {
name: 'Move', name: 'Move',
value: 'move', value: 'move',
description: 'Move a file', description: 'Move a file'
}, },
{ {
name: 'Upload', name: 'Upload',
value: 'upload', value: 'upload',
description: 'Upload a file', description: 'Upload a file'
}, }
], ],
default: 'upload', default: 'upload',
description: 'The operation to perform.', description: 'The operation to perform.'
}, },
{ {
@ -103,40 +127,38 @@ export class Dropbox implements INodeType {
type: 'options', type: 'options',
displayOptions: { displayOptions: {
show: { show: {
resource: [ resource: ['folder']
'folder', }
],
},
}, },
options: [ options: [
{ {
name: 'Copy', name: 'Copy',
value: 'copy', value: 'copy',
description: 'Copy a folder', description: 'Copy a folder'
}, },
{ {
name: 'Create', name: 'Create',
value: 'create', value: 'create',
description: 'Create a folder', description: 'Create a folder'
}, },
{ {
name: 'Delete', name: 'Delete',
value: 'delete', value: 'delete',
description: 'Delete a folder', description: 'Delete a folder'
}, },
{ {
name: 'List', name: 'List',
value: 'list', value: 'list',
description: 'Return the files and folders in a given folder', description: 'Return the files and folders in a given folder'
}, },
{ {
name: 'Move', name: 'Move',
value: 'move', value: 'move',
description: 'Move a folder', description: 'Move a folder'
}, }
], ],
default: 'create', default: 'create',
description: 'The operation to perform.', description: 'The operation to perform.'
}, },
// ---------------------------------- // ----------------------------------
@ -154,17 +176,12 @@ export class Dropbox implements INodeType {
required: true, required: true,
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['copy'],
'copy' resource: ['file', 'folder']
], }
resource: [
'file',
'folder',
],
},
}, },
placeholder: '/invoices/original.txt', placeholder: '/invoices/original.txt',
description: 'The path of file or folder to copy.', description: 'The path of file or folder to copy.'
}, },
{ {
displayName: 'To Path', displayName: 'To Path',
@ -174,17 +191,12 @@ export class Dropbox implements INodeType {
required: true, required: true,
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['copy'],
'copy' resource: ['file', 'folder']
], }
resource: [
'file',
'folder',
],
},
}, },
placeholder: '/invoices/copy.txt', placeholder: '/invoices/copy.txt',
description: 'The destination path of file or folder.', description: 'The destination path of file or folder.'
}, },
// ---------------------------------- // ----------------------------------
@ -198,20 +210,15 @@ export class Dropbox implements INodeType {
required: true, required: true,
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['delete'],
'delete' resource: ['file', 'folder']
], }
resource: [
'file',
'folder',
],
},
}, },
placeholder: '/invoices/2019/invoice_1.pdf', placeholder: '/invoices/2019/invoice_1.pdf',
description: 'The path to delete. Can be a single file or a whole folder.', description:
'The path to delete. Can be a single file or a whole folder.'
}, },
// ---------------------------------- // ----------------------------------
// file/folder:move // file/folder:move
// ---------------------------------- // ----------------------------------
@ -223,17 +230,12 @@ export class Dropbox implements INodeType {
required: true, required: true,
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['move'],
'move' resource: ['file', 'folder']
], }
resource: [
'file',
'folder',
],
},
}, },
placeholder: '/invoices/old_name.txt', placeholder: '/invoices/old_name.txt',
description: 'The path of file or folder to move.', description: 'The path of file or folder to move.'
}, },
{ {
displayName: 'To Path', displayName: 'To Path',
@ -243,17 +245,12 @@ export class Dropbox implements INodeType {
required: true, required: true,
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['move'],
'move' resource: ['file', 'folder']
], }
resource: [
'file',
'folder',
],
},
}, },
placeholder: '/invoices/new_name.txt', placeholder: '/invoices/new_name.txt',
description: 'The new path of file or folder.', description: 'The new path of file or folder.'
}, },
// ---------------------------------- // ----------------------------------
@ -267,16 +264,13 @@ export class Dropbox implements INodeType {
required: true, required: true,
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['download'],
'download' resource: ['file']
], }
resource: [
'file',
],
},
}, },
placeholder: '/invoices/2019/invoice_1.pdf', placeholder: '/invoices/2019/invoice_1.pdf',
description: 'The file path of the file to download. Has to contain the full path.', description:
'The file path of the file to download. Has to contain the full path.'
}, },
{ {
displayName: 'Binary Property', displayName: 'Binary Property',
@ -286,15 +280,12 @@ export class Dropbox implements INodeType {
default: 'data', default: 'data',
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['download'],
'download' resource: ['file']
], }
resource: [
'file',
],
}, },
}, description:
description: 'Name of the binary property to which to<br />write the data of the read file.', 'Name of the binary property to which to<br />write the data of the read file.'
}, },
// ---------------------------------- // ----------------------------------
@ -308,16 +299,13 @@ export class Dropbox implements INodeType {
required: true, required: true,
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['upload'],
'upload' resource: ['file']
], }
resource: [
'file',
],
},
}, },
placeholder: '/invoices/2019/invoice_1.pdf', placeholder: '/invoices/2019/invoice_1.pdf',
description: 'The file path of the file to upload. Has to contain the full path. The parent folder has to exist. Existing files get overwritten.', description:
'The file path of the file to upload. Has to contain the full path. The parent folder has to exist. Existing files get overwritten.'
}, },
{ {
displayName: 'Binary Data', displayName: 'Binary Data',
@ -326,15 +314,11 @@ export class Dropbox implements INodeType {
default: false, default: false,
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['upload'],
'upload' resource: ['file']
], }
resource: [
'file',
],
}, },
}, description: 'If the data to upload should be taken from binary field.'
description: 'If the data to upload should be taken from binary field.',
}, },
{ {
displayName: 'File Content', displayName: 'File Content',
@ -343,20 +327,13 @@ export class Dropbox implements INodeType {
default: '', default: '',
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['upload'],
'upload' resource: ['file'],
], binaryData: [false]
resource: [ }
'file',
],
binaryData: [
false
],
},
}, },
placeholder: '', placeholder: '',
description: 'The text content of the file to upload.', description: 'The text content of the file to upload.'
}, },
{ {
displayName: 'Binary Property', displayName: 'Binary Property',
@ -366,24 +343,16 @@ export class Dropbox implements INodeType {
required: true, required: true,
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['upload'],
'upload' resource: ['file'],
], binaryData: [true]
resource: [ }
'file',
],
binaryData: [
true
],
},
}, },
placeholder: '', placeholder: '',
description: 'Name of the binary property which contains<br />the data for the file to be uploaded.', description:
'Name of the binary property which contains<br />the data for the file to be uploaded.'
}, },
// ---------------------------------- // ----------------------------------
// folder // folder
// ---------------------------------- // ----------------------------------
@ -399,16 +368,12 @@ export class Dropbox implements INodeType {
required: true, required: true,
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['create'],
'create' resource: ['folder']
], }
resource: [
'folder',
],
},
}, },
placeholder: '/invoices/2019', placeholder: '/invoices/2019',
description: 'The folder to create. The parent folder has to exist.', description: 'The folder to create. The parent folder has to exist.'
}, },
// ---------------------------------- // ----------------------------------
@ -421,47 +386,31 @@ export class Dropbox implements INodeType {
default: '', default: '',
displayOptions: { displayOptions: {
show: { show: {
operation: [ operation: ['list'],
'list' resource: ['folder']
], }
resource: [
'folder',
],
},
}, },
placeholder: '/invoices/2019/', placeholder: '/invoices/2019/',
description: 'The path of which to list the content.', description: 'The path of which to list the content.'
}, }
]
],
}; };
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> { async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData(); const items = this.getInputData();
const returnData: IDataObject[] = []; const returnData: IDataObject[] = [];
const credentials = this.getCredentials('dropboxApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
const resource = this.getNodeParameter('resource', 0) as string; const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string; const operation = this.getNodeParameter('operation', 0) as string;
let endpoint = ''; let endpoint = '';
let requestMethod = ''; let requestMethod = '';
let body: IDataObject | Buffer; let body: IDataObject | Buffer;
let isJson = false;
let headers: IDataObject; const headers: IDataObject = {};
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
body = {}; body = {};
headers = {
'Authorization': `Bearer ${credentials.accessToken}`,
};
if (resource === 'file') { if (resource === 'file') {
if (operation === 'download') { if (operation === 'download') {
@ -471,11 +420,10 @@ export class Dropbox implements INodeType {
requestMethod = 'POST'; requestMethod = 'POST';
headers['Dropbox-API-Arg'] = JSON.stringify({ headers['Dropbox-API-Arg'] = JSON.stringify({
path: this.getNodeParameter('path', i) as string, path: this.getNodeParameter('path', i) as string
}); });
endpoint = 'https://content.dropboxapi.com/2/files/download'; endpoint = 'https://content.dropboxapi.com/2/files/download';
} else if (operation === 'upload') { } else if (operation === 'upload') {
// ---------------------------------- // ----------------------------------
// upload // upload
@ -485,7 +433,7 @@ export class Dropbox implements INodeType {
headers['Content-Type'] = 'application/octet-stream'; headers['Content-Type'] = 'application/octet-stream';
headers['Dropbox-API-Arg'] = JSON.stringify({ headers['Dropbox-API-Arg'] = JSON.stringify({
mode: 'overwrite', mode: 'overwrite',
path: this.getNodeParameter('path', i) as string, path: this.getNodeParameter('path', i) as string
}); });
endpoint = 'https://content.dropboxapi.com/2/files/upload'; endpoint = 'https://content.dropboxapi.com/2/files/upload';
@ -498,19 +446,29 @@ export class Dropbox implements INodeType {
throw new Error('No binary data exists on item!'); throw new Error('No binary data exists on item!');
} }
const propertyNameUpload = this.getNodeParameter('binaryPropertyName', i) as string; const propertyNameUpload = this.getNodeParameter(
'binaryPropertyName',
i
) as string;
if (item.binary[propertyNameUpload] === undefined) { if (item.binary[propertyNameUpload] === undefined) {
throw new Error(`No binary data property "${propertyNameUpload}" does not exists on item!`); throw new Error(
`No binary data property "${propertyNameUpload}" does not exists on item!`
);
} }
body = Buffer.from(item.binary[propertyNameUpload].data, BINARY_ENCODING); body = Buffer.from(
item.binary[propertyNameUpload].data,
BINARY_ENCODING
);
} else { } else {
// Is text file // Is text file
body = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'utf8'); body = Buffer.from(
this.getNodeParameter('fileContent', i) as string,
'utf8'
);
} }
} }
} else if (resource === 'folder') { } else if (resource === 'folder') {
if (operation === 'create') { if (operation === 'create') {
// ---------------------------------- // ----------------------------------
@ -518,30 +476,26 @@ export class Dropbox implements INodeType {
// ---------------------------------- // ----------------------------------
requestMethod = 'POST'; requestMethod = 'POST';
isJson = true;
body = { body = {
path: this.getNodeParameter('path', i) as string, path: this.getNodeParameter('path', i) as string
}; };
endpoint = 'https://api.dropboxapi.com/2/files/create_folder_v2'; endpoint = 'https://api.dropboxapi.com/2/files/create_folder_v2';
} else if (operation === 'list') { } else if (operation === 'list') {
// ---------------------------------- // ----------------------------------
// list // list
// ---------------------------------- // ----------------------------------
requestMethod = 'POST'; requestMethod = 'POST';
isJson = true;
body = { body = {
path: this.getNodeParameter('path', i) as string, path: this.getNodeParameter('path', i) as string,
limit: 2000, limit: 2000
}; };
// TODO: If more files than the max-amount exist it has to be possible to // TODO: If more files than the max-amount exist it has to be possible to
// also request them. // also request them.
endpoint = 'https://api.dropboxapi.com/2/files/list_folder'; endpoint = 'https://api.dropboxapi.com/2/files/list_folder';
} }
} }
if (['file', 'folder'].includes(resource)) { if (['file', 'folder'].includes(resource)) {
@ -551,37 +505,32 @@ export class Dropbox implements INodeType {
// ---------------------------------- // ----------------------------------
requestMethod = 'POST'; requestMethod = 'POST';
isJson = true;
body = { body = {
from_path: this.getNodeParameter('path', i) as string, from_path: this.getNodeParameter('path', i) as string,
to_path: this.getNodeParameter('toPath', i) as string, to_path: this.getNodeParameter('toPath', i) as string
}; };
endpoint = 'https://api.dropboxapi.com/2/files/copy_v2'; endpoint = 'https://api.dropboxapi.com/2/files/copy_v2';
} else if (operation === 'delete') { } else if (operation === 'delete') {
// ---------------------------------- // ----------------------------------
// delete // delete
// ---------------------------------- // ----------------------------------
requestMethod = 'POST'; requestMethod = 'POST';
isJson = true;
body = { body = {
path: this.getNodeParameter('path', i) as string, path: this.getNodeParameter('path', i) as string
}; };
endpoint = 'https://api.dropboxapi.com/2/files/delete_v2'; endpoint = 'https://api.dropboxapi.com/2/files/delete_v2';
} else if (operation === 'move') { } else if (operation === 'move') {
// ---------------------------------- // ----------------------------------
// move // move
// ---------------------------------- // ----------------------------------
requestMethod = 'POST'; requestMethod = 'POST';
isJson = true;
body = { body = {
from_path: this.getNodeParameter('path', i) as string, from_path: this.getNodeParameter('path', i) as string,
to_path: this.getNodeParameter('toPath', i) as string, to_path: this.getNodeParameter('toPath', i) as string
}; };
endpoint = 'https://api.dropboxapi.com/2/files/move_v2'; endpoint = 'https://api.dropboxapi.com/2/files/move_v2';
@ -590,47 +539,25 @@ export class Dropbox implements INodeType {
throw new Error(`The resource "${resource}" is not known!`); throw new Error(`The resource "${resource}" is not known!`);
} }
let encoding: string | null = '';
const options: OptionsWithUri = {
headers,
method: requestMethod,
qs: {},
uri: endpoint,
json: isJson,
};
if (Object.keys(body).length) {
options.body = body;
}
if (resource === 'file' && operation === 'download') { if (resource === 'file' && operation === 'download') {
// Return the data as a buffer // Return the data as a buffer
options.encoding = null; encoding = null;
} }
let responseData; const responseData = await dropboxApiRequest.call(
try { this,
responseData = await this.helpers.request(options); requestMethod,
} catch (error) { endpoint,
if (error.statusCode === 401) { body,
// Return a clear error headers,
throw new Error('The Dropbox credentials are not valid!'); encoding
} );
if (error.error && error.error.error_summary) {
// Try to return the error prettier
throw new Error(`Dropbox error response [${error.statusCode}]: ${error.error.error_summary}`);
}
// If that data does not exist for some reason return the actual error
throw error;
}
if (resource === 'file' && operation === 'download') { if (resource === 'file' && operation === 'download') {
const newItem: INodeExecutionData = { const newItem: INodeExecutionData = {
json: items[i].json, json: items[i].json,
binary: {}, binary: {}
}; };
if (items[i].binary !== undefined) { if (items[i].binary !== undefined) {
@ -642,22 +569,28 @@ export class Dropbox implements INodeType {
items[i] = newItem; items[i] = newItem;
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', i) as string; const dataPropertyNameDownload = this.getNodeParameter(
'binaryPropertyName',
i
) as string;
const filePathDownload = this.getNodeParameter('path', i) as string; const filePathDownload = this.getNodeParameter('path', i) as string;
items[i].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(responseData, filePathDownload); items[i].binary![
dataPropertyNameDownload
] = await this.helpers.prepareBinaryData(
Buffer.from(responseData.data),
filePathDownload
);
} else if (resource === 'folder' && operation === 'list') { } else if (resource === 'folder' && operation === 'list') {
const propNames: { [key: string]: string } = { const propNames: { [key: string]: string } = {
'id': 'id', id: 'id',
'name': 'name', name: 'name',
'client_modified': 'lastModifiedClient', client_modified: 'lastModifiedClient',
'server_modified': 'lastModifiedServer', server_modified: 'lastModifiedServer',
'rev': 'rev', rev: 'rev',
'size': 'contentSize', size: 'contentSize',
'.tag': 'type', '.tag': 'type',
'content_hash': 'contentHash', content_hash: 'contentHash'
}; };
for (const item of responseData.entries) { for (const item of responseData.entries) {
@ -673,7 +606,7 @@ export class Dropbox implements INodeType {
returnData.push(newItem as IDataObject); returnData.push(newItem as IDataObject);
} }
} else if (resource === 'file' && operation === 'upload') { } else if (resource === 'file' && operation === 'upload') {
returnData.push(JSON.parse(responseData) as IDataObject); returnData.push(responseData as IDataObject);
} else { } else {
returnData.push(responseData as IDataObject); returnData.push(responseData as IDataObject);
} }

View file

@ -0,0 +1,71 @@
import { IExecuteFunctions, IHookFunctions } from 'n8n-core';
import { OptionsWithUri } from 'request';
/**
* Make an API request to Dropbox
*
* @param {IHookFunctions} this
* @param {string} method
* @param {string} url
* @param {object} body
* @returns {Promise<any>}
*/
export async function dropboxApiRequest(
this: IHookFunctions | IExecuteFunctions,
method: string,
endpoint: string,
body: object,
headers?: object,
encoding?: string | null
): Promise<any> {
// tslint:disable-line:no-any
const options: OptionsWithUri = {
headers,
method,
body,
uri: endpoint,
json: true,
encoding
};
if (!Object.keys(body).length) {
delete options.body;
}
if (encoding !== null) {
delete options.encoding;
}
const authenticationMethod = this.getNodeParameter(
'authentication',
0
) as string;
try {
if (authenticationMethod === 'accessToken') {
return await this.helpers.request(options);
} else {
return await this.helpers.requestOAuth.call(
this,
'dropboxOAuth2Api',
options
);
}
} catch (error) {
if (error.statusCode === 401) {
// Return a clear error
throw new Error('The Dropbox credentials are not valid!');
}
if (error.error && error.error.error_summary) {
// Try to return the error prettier
throw new Error(
`Dropbox error response [${error.statusCode}]: ${error.error.error_summary}`
);
}
// If that data does not exist for some reason return the actual error
throw error;
}
}

View file

@ -48,6 +48,7 @@
"dist/credentials/DisqusApi.credentials.js", "dist/credentials/DisqusApi.credentials.js",
"dist/credentials/DriftApi.credentials.js", "dist/credentials/DriftApi.credentials.js",
"dist/credentials/DropboxApi.credentials.js", "dist/credentials/DropboxApi.credentials.js",
"dist/credentials/DropboxOAuth2Api.credentials.js",
"dist/credentials/EventbriteApi.credentials.js", "dist/credentials/EventbriteApi.credentials.js",
"dist/credentials/FacebookGraphApi.credentials.js", "dist/credentials/FacebookGraphApi.credentials.js",
"dist/credentials/FreshdeskApi.credentials.js", "dist/credentials/FreshdeskApi.credentials.js",