diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 071f61ab5a597..3a5e8bf38d6c1 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -729,6 +729,10 @@ function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequest axiosRequest.headers['User-Agent'] = 'n8n'; } + if (n8nRequest.validateStatus !== undefined) { + axiosRequest.validateStatus = n8nRequest.validateStatus; + } + return axiosRequest; } diff --git a/packages/nodes-base/credentials/WiseApi.credentials.ts b/packages/nodes-base/credentials/WiseApi.credentials.ts index f3d558771bdd1..ce0a2723b024f 100644 --- a/packages/nodes-base/credentials/WiseApi.credentials.ts +++ b/packages/nodes-base/credentials/WiseApi.credentials.ts @@ -30,5 +30,15 @@ export class WiseApi implements ICredentialType { }, ], }, + { + displayName: 'Private Key (Optional)', + name: 'privateKey', + type: 'string', + default: '', + description: 'Optional private key used for Strong Customer Authentication (SCA). Only needed to retrieve statements, and execute transfers.', + typeOptions: { + alwaysOpenEditWindow: true, + }, + }, ]; } diff --git a/packages/nodes-base/nodes/Wise/GenericFunctions.ts b/packages/nodes-base/nodes/Wise/GenericFunctions.ts index 785a2097b43f5..6131645690594 100644 --- a/packages/nodes-base/nodes/Wise/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Wise/GenericFunctions.ts @@ -1,3 +1,7 @@ +import { + createSign, +} from 'crypto'; + import { IExecuteFunctions, IHookFunctions, @@ -5,45 +9,47 @@ import { import { IDataObject, + IHttpRequestOptions, ILoadOptionsFunctions, INodeExecutionData, NodeApiError, } from 'n8n-workflow'; -import { - OptionsWithUri, -} from 'request'; - /** * Make an authenticated API request to Wise. */ export async function wiseApiRequest( this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, - method: string, + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'PATCH', endpoint: string, body: IDataObject = {}, qs: IDataObject = {}, option: IDataObject = {}, ) { - const { apiToken, environment } = await this.getCredentials('wiseApi') as { + const { apiToken, environment, privateKey } = await this.getCredentials('wiseApi') as { apiToken: string, environment: 'live' | 'test', + privateKey?: string, }; const rootUrl = environment === 'live' ? 'https://api.transferwise.com/' : 'https://api.sandbox.transferwise.tech/'; - const options: OptionsWithUri = { + const options: IHttpRequestOptions = { headers: { 'user-agent': 'n8n', 'Authorization': `Bearer ${apiToken}`, }, method, - uri: `${rootUrl}${endpoint}`, + url: `${rootUrl}${endpoint}`, qs, body, json: true, + returnFullResponse: true, + validateStatus: (status) => { + return (status >= 200 && status < 300) || status === 403; + }, }; if (!Object.keys(body).length) { @@ -58,11 +64,55 @@ export async function wiseApiRequest( Object.assign(options, option); } + let response; try { - return await this.helpers.request!(options); + response = await this.helpers.httpRequest!(options); } catch (error) { + delete error.config; throw new NodeApiError(this.getNode(), error); } + + if (response.statusCode === 200) { + return response.body; + } + + // Request requires SCA approval + if (response.statusCode === 403 && response.headers['x-2fa-approval']) { + if (!privateKey) { + throw new NodeApiError(this.getNode(), { + message: 'This request requires Strong Customer Authentication (SCA). Please add a key pair to your account and n8n credentials. See https://api-docs.transferwise.com/#strong-customer-authentication-personal-token', + headers: response.headers, + body: response.body, + }); + } + // Sign the x-2fa-approval + const oneTimeToken = response.headers['x-2fa-approval'] as string; + const signerObject = createSign('RSA-SHA256'); + signerObject.update(oneTimeToken); + try{ + const signature = signerObject.sign( + privateKey, + 'base64', + ); + delete options.validateStatus; + options.headers = { + ...options.headers, + 'X-Signature': signature, + 'x-2fa-approval': oneTimeToken, + }; + } catch (error) { + throw new NodeApiError(this.getNode(), {message: 'Error signing SCA request, check your private key', ...error}); + } + // Retry the request with signed token + try { + response = await this.helpers.httpRequest!(options); + return response.body; + } catch (error) { + throw new NodeApiError(this.getNode(), {message: 'SCA request failed, check your private key is valid'}); + } + } else { + throw new NodeApiError(this.getNode(), {headers: response.headers, body: response.body},); + } } /** @@ -113,8 +163,12 @@ export type Profile = { }; export type Recipient = { + active: boolean, id: number, - accountHolderName: string + accountHolderName: string, + country: string | null, + currency: string, + type: string, }; export type StatementAdditionalFields = { diff --git a/packages/nodes-base/nodes/Wise/Wise.node.ts b/packages/nodes-base/nodes/Wise/Wise.node.ts index 34917427357ce..5011eb586b5d4 100644 --- a/packages/nodes-base/nodes/Wise/Wise.node.ts +++ b/packages/nodes-base/nodes/Wise/Wise.node.ts @@ -5,6 +5,7 @@ import { import { IDataObject, ILoadOptionsFunctions, + INodePropertyOptions, INodeType, INodeTypeDescription, } from 'n8n-workflow'; @@ -141,12 +142,25 @@ export class Wise implements INodeType { profileId: this.getNodeParameter('profileId', 0), }; - const recipients = await wiseApiRequest.call(this, 'GET', 'v1/accounts', {}, qs); - - return recipients.map(({ id, accountHolderName }: Recipient) => ({ - name: accountHolderName, - value: id, - })); + const recipients = await wiseApiRequest.call(this, 'GET', 'v1/accounts', {}, qs) as Recipient[]; + + return recipients.reduce((activeRecipients: INodePropertyOptions[], { + active, + id, + accountHolderName, + currency, + country, + type, + }: Recipient) => { + if (active) { + const recipient = { + name: `[${currency}] ${accountHolderName} - (${country !== null ? country + ' - ' : '' }${type})`, + value: id, + }; + activeRecipients.push(recipient); + } + return activeRecipients; + }, []); }, }, }; diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index c8bf57f60dae4..652907b78ee50 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -327,6 +327,7 @@ export interface IHttpRequestOptions { }; timeout?: number; json?: boolean; + validateStatus?: ((status: number) => boolean) | null; } export type IN8nHttpResponse = IDataObject | Buffer | GenericValue | GenericValue[] | null;