Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support SCA-protected endpoints #281

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,7 @@ dist/
*.xml

# Jest
.jest/
.jest/

# macOS
.DS_Store
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,18 @@ const tw = new TransferWise({ token: ..., sandbox: true });
const profiles = await tw.profiles();
```

Optionally, in order to support [SCA-protected](https://api-docs.transferwise.com/#strong-customer-authentication) endpoints, it is possible to pass a PEM-formatted private key to the constructor (`new TransferWise({ token: ..., privateKey: ... })`).

## Methods

Currently only supports methods listed below. Aim to support all API methods _soon_.

**me**

```js
await tw.me();
```

**profiles**

```js
Expand All @@ -47,6 +55,12 @@ await tw.profiles();
await tw.borderlessAccounts("<profileId>");
```

**statement**

```js
await tw.statement("<profileId>", "<borderlessAccountId>", "<iso4217CurrencyCode>", {"<interval>"});
```

**recipientAccounts**

```js
Expand Down
8 changes: 7 additions & 1 deletion __tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,13 @@ describe("TransferWise", () => {

describe("request", () => {
beforeEach(() => {
fetch.mockReturnValue({ then: jest.fn() });
fetch.mockReturnValue(
Promise.resolve({
json: () => {
return { };
},
})
);
});

test("it calls fetch with defaults", () => {
Expand Down
52 changes: 48 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fetch from "node-fetch";
import { encode } from "querystring";
import crypto from "crypto";
const encode = params => new URLSearchParams(params).toString();
import { constructEvent } from "./webhooks";

const SANDBOX_URL = "https://api.sandbox.transferwise.tech";
Expand All @@ -8,12 +9,13 @@ const LIVE_URL = "https://api.transferwise.com";
const VERSION = "v1";

class TransferWise {
constructor({ token, sandbox = false } = {}) {
constructor({ token, privateKey = false, sandbox = false } = {}) {
if (!token) throw new Error("token is required");

this.token = token;
this.sandbox = sandbox;
this.url = sandbox ? SANDBOX_URL : LIVE_URL;
this.privateKey = privateKey
}

request({ method = "GET", path = "", body, version } = {}) {
Expand All @@ -29,8 +31,29 @@ class TransferWise {
}
};
if (body) fetchOptions.body = JSON.stringify(body);
if (method === "DELETE") return fetch(url, fetchOptions);
return fetch(url, fetchOptions).then(resp => resp.json());

const scaRequestHandler = resp => {
if (resp.status !== 403) return resp;
if (resp.headers.get('X-2FA-Approval-Result') !== 'REJECTED') return resp;

if (this.privateKey === false) {
throw new Error("private key is required to access SCA-protected endpoints");
}

const oneTimeToken = resp.headers.get('X-2FA-Approval');
const signedOTT = this.constructor.utils.signOTT(this.privateKey, oneTimeToken);
fetchOptions.headers["X-2FA-Approval"] = oneTimeToken;
fetchOptions.headers["X-Signature"] = signedOTT;
return fetch(url, fetchOptions);
};

const scaCapableFetch = () => fetch(url, fetchOptions).then(scaRequestHandler);
if (method === "DELETE") return scaCapableFetch();
return scaCapableFetch().then(resp => resp.json());
}

me() {
return this.request({ path: "/me" });
}

profiles() {
Expand All @@ -43,6 +66,17 @@ class TransferWise {
});
}

statement(profileId, accountId, currency, { intervalStart, intervalEnd }) {
const params = {
currency,
intervalStart: intervalStart.toISOString(),
intervalEnd: intervalEnd.toISOString(),
type: "COMPACT"
};

return this.request({ path: `/profiles/${profileId}/borderless-accounts/${accountId}/statement.json?${encode(params)}`, version: "v3" });
}

get recipientAccounts() {
return {
create: accountDetails =>
Expand Down Expand Up @@ -129,6 +163,16 @@ class TransferWise {
constructEvent(this.sandbox, body, signature)
};
}

static get utils() {
return {
signOTT(pKey, ott) {
const sign = crypto.createSign("RSA-SHA256");
sign.update(ott);
return sign.sign(pKey).toString("base64");
}
};
}
}

export default TransferWise;
Loading