Skip to content

Commit

Permalink
Merge pull request #185 from gocardless/template-changes
Browse files Browse the repository at this point in the history
Changes generated by bb1f4e40bb6cc691c72632f0a9aae6ddd655b23f
  • Loading branch information
prolific117 authored May 30, 2024
2 parents b9c7359 + 7ac1e4b commit 47d9db9
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 258 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ For a full list of available resources, visit the [GoCardless API reference](htt
```js
const uuidv4 = require('uuid/v4');

// Create a new payment with an idempotency key
// Create a new payment.
const payment = await client.payments.create(
{
amount: 100,
currency: "GBP",
links: { mandate: "MD123" },
},
uuidv4(),
{ uuidv4() },
);

// List the first three payments past a certain date.
Expand Down
343 changes: 167 additions & 176 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 8 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "gocardless-nodejs",
"version": "3.23.0",
"version": "3.24.0",
"description": "Node.js client for the GoCardless API - a powerful, simple solution for the collection of recurring bank-to-bank payments",
"author": "GoCardless Ltd <[email protected]>",
"repository": {
Expand All @@ -25,21 +25,23 @@
"dependencies": {
"got": "^11.8.5",
"lodash": "^4.17.15",
"qs": "^6.9.1",
"uuid": "^7.0.2"
"uuid": "^7.0.2",
"crypto-js": "3.2.1",
"buffer-equal-constant-time": "1.0.1",
"qs": "^6.9.1"
},
"devDependencies": {
"@types/node": "^13.9.0",
"@types/jest": "^25.1.4",
"@types/nock": "^11.1.0",
"@types/node": "^13.9.0",
"jest": "^25.1.0",
"nock": "^13.0.11",
"ts-jest": "25.2.1",
"nock": "^13.0.11",
"typescript": "^4.2.4"
},
"main": "index.js",
"types": "types/Types.d.ts",
"engines": {
"node": ">=18.0"
"node": ">=10.0"
}
}
24 changes: 24 additions & 0 deletions src/api/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,30 @@ describe(".request", () => {
});
});

describe("NoResponseInFailureError", () =>{
it("throws the correct error", async () => {
const check = nock("https://api.gocardless.com").
post("/").
reply(422, '', { 'Content-Type': 'application/json' });

const params = { path: "/", method: "post", fetch: null }
const api = new Api(token, environment, {});

let e;
try {
await api.request(params);
} catch (err) {
e = err;
}

expect(e).toBeInstanceOf(GoCardlessErrors.GoCardlessInternalError);
expect(e.toString()).
toEqual("Internal server error")

expect(check.isDone()).toEqual(true);
});
});

describe("ValidationFailedError", () =>{
it("throws the correct error", async () => {
const check = nock("https://api.gocardless.com").
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ enum Environments {
Sandbox = 'SANDBOX',
}

const CLIENT_VERSION = '3.23.0';
const CLIENT_VERSION = '3.24.0';
const API_VERSION = '2015-07-06';

export { Environments, CLIENT_VERSION, API_VERSION };
110 changes: 67 additions & 43 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,53 +57,77 @@ class ApiError extends GoCardlessException {
}

static buildFromResponse(response) {
const {
statusCode,
body: {
error: { type, errors },
},
} = response;

// These statuses are for unique errors
switch (statusCode) {
case 401:
return new AuthenticationError(response);
case 403:
return new PermissionsError(response);
case 429:
return new RateLimitError(response);
default:
//noop
}

// Whereas these errors have different meanings over the same codes
switch (type) {
case 'validation_failed':
return new ValidationFailedError(response);
case 'invalid_api_usage':
return new InvalidApiUsageError(response);
case 'invalid_state':
for (const e of errors) {
if (e.reason === 'idempotent_creation_conflict') {
if (e.links && e.links.conflicting_resource_id) {
return new IdempotentCreationConflictError(
response,
e.links.conflicting_resource_id
try {
const {
statusCode,
body: {
error: { type, errors },
},
} = response;

// These statuses are for unique errors
switch (statusCode) {
case 401:
return new AuthenticationError(response);
case 403:
return new PermissionsError(response);
case 429:
return new RateLimitError(response);
default:
//noop
}

// Whereas these errors have different meanings over the same codes
switch (type) {
case 'validation_failed':
return new ValidationFailedError(response);
case 'invalid_api_usage':
return new InvalidApiUsageError(response);
case 'invalid_state':
for (const e of errors) {
if (e.reason === 'idempotent_creation_conflict') {
if (e.links && e.links.conflicting_resource_id) {
return new IdempotentCreationConflictError(
response,
e.links.conflicting_resource_id
);
}

return new MalformedResponseError(
'Idempotent Creation Conflict Error missing conflicting_resource_id',
response
);
}

return new MalformedResponseError(
'Idempotent Creation Conflict Error missing conflicting_resource_id',
response
);
}
}

return new InvalidStateError(response);
case 'gocardless':
return new GoCardlessInternalError(response);
default:
return new ApiError(response);
return new InvalidStateError(response);
case 'gocardless':
return new GoCardlessInternalError(response);
default:
return new ApiError(response);
}
} catch (err) {
const failureResponse = {
statusCode: 500,
body: {
error: {
message: 'Internal server error',
errors: [
{
reason: 'internal_server_error',
message: 'Internal server error',
},
],
documentation_url:
'https://developer.gocardless.com/api-reference#internal_server_error',
type: 'gocardless',
request_id: response.headers['x-request-id'],
code: 500,
},
},
};

return new GoCardlessInternalError(failureResponse);
}
}

Expand Down
13 changes: 1 addition & 12 deletions src/webhooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ const requestBody = JSON.stringify(
JSON.parse(fs.readFileSync("src/fixtures/webhook_body.json", "utf8"))
);

const requestBodyBuffer = Buffer.from(requestBody);

const webhookSecret = "ED7D658C-D8EB-4941-948B-3973214F2D49"
const signatureHeader = "2693754819d3e32d7e8fcb13c729631f316c6de8dc1cf634d6527f1c07276e7e";


describe(".parse", () => {
test("parses a string body with valid signature", () => {
test("parses a webhook response body with valid signature", () => {
const result = webhook.parse(requestBody, webhookSecret, signatureHeader);

expect(result.length).toBe(2);
Expand All @@ -21,15 +19,6 @@ describe(".parse", () => {
expect(firstEvent.id).toBe("EV00BD05S5VM2T");
});

test("parses a buffer body with valid signature", () => {
const result = webhook.parse(requestBodyBuffer, webhookSecret, signatureHeader);

expect(result.length).toBe(2);

const firstEvent = result[0];
expect(firstEvent.id).toBe("EV00BD05S5VM2T");
});

test("parses a webhook response body with an invalid signature", () => {
const badSignatureHeader = "NOTVERYCONVINCING";

Expand Down
37 changes: 19 additions & 18 deletions src/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
* JSON object into an `GoCardless.Event` class.
*/

import crypto from 'crypto';
import type { Event } from './types/Types';
import cryptoJS from 'crypto-js';
import safeCompare from 'buffer-equal-constant-time';

function InvalidSignatureError() {
this.message =
Expand All @@ -23,41 +23,42 @@ function InvalidSignatureError() {
* Validates that a webhook was genuinely sent by GoCardless, then parses each `event`
* object into an array of `GoCardless.Event` classes.
*
* @body The raw webhook body.
* @webhookSecret The webhook endpoint secret for your webhook endpoint, as
* @body [string]: The raw webhook body.
* @webhookSecret [string]: The webhook endpoint secret for your webhook endpoint, as
* configured in your GoCardless Dashboard.
* @signatureHeader The signature included in the webhook request, as specified
* @signatureHeader [string]: The signature included in the webhook request, as specified
* by the `Webhook-Signature` header.
*/
function parse(body: crypto.BinaryLike, webhookSecret: string, signatureHeader: string): Event[] {
function parse(body: string, webhookSecret: string, signatureHeader: string) {
verifySignature(body, webhookSecret, signatureHeader);

const bodyString = typeof body === 'string' ? body : body.toString();
const eventsData = JSON.parse(bodyString) as { events: Event[] };
return eventsData.events;
const eventsData = JSON.parse(body)['events'];
return eventsData.map(eventJson => eventJson);
}

/**
* Validate the signature header. Note, we're using the `crypto.timingSafeEqual`
* Validate the signature header. Note, we're using the `buffer-equal-constant-time`
* library for the hash comparison, to protect against timing attacks.
*
* @body The raw webhook body.
* @webhookSecret The webhook endpoint secret for your webhook endpoint, as
* @body [string]: The raw webhook body.
* @webhookSecret [string]: The webhook endpoint secret for your webhook endpoint, as
* configured in your GoCardless Dashboard.
* @signatureHeader The signature included in the webhook request, as specified
* @signatureHeader [string]: The signature included in the webhook request, as specified
* by the `Webhook-Signature` header.
*/
function verifySignature(
body: crypto.BinaryLike,
body: string,
webhookSecret: string,
signatureHeader: string
) {
const bufferDigest = crypto.createHmac('sha256', webhookSecret).update(body).digest();
const bufferSignatureHeader = Buffer.from(signatureHeader, 'hex');
const rawDigest = cryptoJS.HmacSHA256(body, webhookSecret);

if ((bufferDigest.length !== bufferSignatureHeader.length) || !crypto.timingSafeEqual(bufferDigest, bufferSignatureHeader)) {
const bufferDigest = Buffer.from(rawDigest.toString(cryptoJS.enc.Hex));
const bufferSignatureHeader = Buffer.from(signatureHeader);

if (!safeCompare(bufferDigest, bufferSignatureHeader)) {
throw new InvalidSignatureError();
}
}

export { parse, verifySignature, InvalidSignatureError };
export { parse, InvalidSignatureError };

0 comments on commit 47d9db9

Please sign in to comment.