diff --git a/README.md b/README.md index bb69169..20f49a0 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ To write an app using the SDK postalCode: "78701", }; - client.createUser(userDate, function(errors, body, res) { + client.createUser(userData, function(errors, body, res) { if (errors) { console.log("Create User Failed"); console.log(errors); diff --git a/src/utils/ApiClient.js b/src/utils/ApiClient.js index 812eb66..b2d83fb 100644 --- a/src/utils/ApiClient.js +++ b/src/utils/ApiClient.js @@ -84,12 +84,12 @@ export default class ApiClient { * @param {api-callback} callback - The callback for this call */ doPost(partialUrl, data, params, callback) { - let contentType = "json"; - let processResponse = this.wrapCallback(callback); + let contentType = "application/json"; + let accept = "application/json"; let requestDataPromise = new Promise((resolve) => resolve(data)); if (this.isEncrypted) { contentType = "application/jose+json"; - processResponse = this.processEncryptedResponse("POST", callback); + accept = "application/jose+json"; requestDataPromise = this.encryption.encrypt(data); } requestDataPromise.then((requestData) => { @@ -98,10 +98,10 @@ export default class ApiClient { .auth(this.username, this.password) .set("User-Agent", `Hyperwallet Node SDK v${this.version}`) .type(contentType) - .accept("json") + .accept(accept) .query(params) .send(requestData) - .end(processResponse); + .end(this.wrapCallback("POST", callback)); }).catch(() => callback("Failed to encrypt body for POST request", undefined, undefined)); } @@ -114,12 +114,12 @@ export default class ApiClient { * @param {api-callback} callback - The callback for this call */ doPut(partialUrl, data, params, callback) { - let contentType = "json"; - let processResponse = this.wrapCallback(callback); + let contentType = "application/json"; + let accept = "application/json"; let requestDataPromise = new Promise((resolve) => resolve(data)); if (this.isEncrypted) { contentType = "application/jose+json"; - processResponse = this.processEncryptedResponse("PUT", callback); + accept = "application/jose+json"; requestDataPromise = this.encryption.encrypt(data); } requestDataPromise.then((requestData) => { @@ -128,10 +128,10 @@ export default class ApiClient { .auth(this.username, this.password) .set("User-Agent", `Hyperwallet Node SDK v${this.version}`) .type(contentType) - .accept("json") + .accept(accept) .query(params) .send(requestData) - .end(processResponse); + .end(this.wrapCallback("PUT", callback)); }).catch(() => callback("Failed to encrypt body for PUT request", undefined, undefined)); } @@ -143,36 +143,49 @@ export default class ApiClient { * @param {api-callback} callback - The callback for this call */ doGet(partialUrl, params, callback) { - let contentType = "json"; + let contentType = "application/json"; + let accept = "application/json"; if (this.isEncrypted) { contentType = "application/jose+json"; + accept = "application/jose+json"; } request .get(`${this.server}/rest/v3/${partialUrl}`) .auth(this.username, this.password) .set("User-Agent", `Hyperwallet Node SDK v${this.version}`) .type(contentType) - .accept("json") + .accept(accept) .query(params) - .end(this.isEncrypted ? this.processEncryptedResponse("GET", callback) : this.wrapCallback(callback)); + .end(this.wrapCallback("GET", callback)); } /** * Wrap a callback to process possible API and network errors * + * @param {string} httpMethod - The http method that is currently processing * @param {api-callback} callback - The final callback * @returns {function(err: Object, res: Object)} - The super agent callback * * @private */ - wrapCallback(callback = () => null) { + wrapCallback(httpMethod, callback = () => null) { return (err, res) => { - this.processResponse(err, res, callback); + if (res && ((!this.isEncrypted && res.type !== "application/json") || (this.isEncrypted && res.type !== "application/jose+json"))) { + callback([{ + message: "Invalid Content-Type specified in Response Header", + }], res ? res.body : undefined, res); + return; + } + if (this.isEncrypted) { + this.processEncryptedResponse(httpMethod, err, res, callback); + } else { + this.processNonEncryptedResponse(err, res, callback); + } }; } /** - * Process response from server + * Process non encrypted response from server * * @param {Object} err - Error object * @param {Object} res - Response object @@ -180,7 +193,7 @@ export default class ApiClient { * * @private */ - processResponse(err, res, callback) { + processNonEncryptedResponse(err, res, callback) { if (!err) { callback(undefined, res.body, res); return; @@ -199,25 +212,30 @@ export default class ApiClient { } /** - * Makes decryption for encrypted response bodies + * Process encrypted response from server * * @param {string} httpMethod - The http method that is currently processing - * @param {api-callback} callback - The callback method to be invoked after decryption + * @param {Object} err - Error object + * @param {Object} res - Response object + * @param {api-callback} callback - The final callback * * @private */ - processEncryptedResponse(httpMethod, callback) { - return (error, response) => { - if (!error) { - const responseBody = response.rawResponse ? response.rawResponse : response.text; - this.encryption.decrypt(responseBody) - .then((decryptedData) => { - callback(undefined, JSON.parse(decryptedData.payload.toString()), decryptedData); - }) - .catch(() => callback(`Failed to decrypt response for ${httpMethod} request`, responseBody, responseBody)); - } else { - this.processResponse(error, response, callback); + processEncryptedResponse(httpMethod, err, res, callback) { + if (!err) { + let responseBody = res.rawResponse ? res.rawResponse : res.text; + try { + responseBody = this.encryption.base64Encode(JSON.parse(res.text)); + } catch (e) { + // nothing to do } - }; + this.encryption.decrypt(responseBody) + .then((decryptedData) => { + callback(undefined, JSON.parse(decryptedData.payload.toString()), decryptedData); + }) + .catch(() => callback(`Failed to decrypt response for ${httpMethod} request`, responseBody, responseBody)); + } else { + this.processNonEncryptedResponse(err, res, callback); + } } } diff --git a/src/utils/Encryption.js b/src/utils/Encryption.js index 13f2458..ebe3d98 100644 --- a/src/utils/Encryption.js +++ b/src/utils/Encryption.js @@ -310,4 +310,33 @@ export default class Encryption { callback(!error && response.statusCode === 200); }); } + + /** + * Convert encrypted string to array of Buffer + * + * @param {string} encryptedBody - Encrypted body to be decoded + */ + base64Decode(encryptedBody) { + const parts = encryptedBody.split("."); + const decodedParts = []; + parts.forEach(elem => { + decodedParts.push(jose.util.base64url.decode(elem)); + }); + const decodedBody = {}; + decodedBody.parts = decodedParts; + return decodedBody; + } + + /** + * Convert array of Buffer to encrypted string + * + * @param {string} decodedBody - Array of Buffer to be decoded to encrypted string + */ + base64Encode(decodedBody) { + const encodedParts = []; + decodedBody.parts.forEach(part => { + encodedParts.push(jose.util.base64url.encode(Buffer.from(JSON.parse(JSON.stringify(part)).data))); + }); + return encodedParts.join("."); + } } diff --git a/test/utils/ApiClient.spec.js b/test/utils/ApiClient.spec.js index 909b1e4..1d2bb94 100644 --- a/test/utils/ApiClient.spec.js +++ b/test/utils/ApiClient.spec.js @@ -176,10 +176,12 @@ describe("utils/ApiClient", () => { .filteringPath(() => "/") .matchHeader("Authorization", authHeader) .matchHeader("User-Agent", `Hyperwallet Node SDK v${packageJson.version}`) - .matchHeader("Accept", "application/json") + .matchHeader("Accept", "application/jose+json") .matchHeader("Content-Type", "application/jose+json") .post("/", /.+/) - .reply(201, encryptedBody); + .reply(200, encryption.base64Decode(encryptedBody), { + "Content-Type": "application/jose+json", + }); clientWithEncryption.doPost("test", { message: "Test message" }, {}, (err, body, res) => { expect(err).to.be.undefined(); @@ -376,10 +378,10 @@ describe("utils/ApiClient", () => { .filteringPath(() => "/") .matchHeader("Authorization", authHeader) .matchHeader("User-Agent", `Hyperwallet Node SDK v${packageJson.version}`) - .matchHeader("Accept", "application/json") + .matchHeader("Accept", "application/jose+json") .matchHeader("Content-Type", "application/jose+json") .put("/", /.+/) - .reply(201, encryptedBody); + .reply(201, encryption.base64Decode(encryptedBody), { "Content-Type": "application/jose+json" }); clientWithEncryption.doPut("test", { message: "Test message" }, {}, (err, body, res) => { expect(err).to.be.undefined(); @@ -448,10 +450,10 @@ describe("utils/ApiClient", () => { .filteringPath(() => "/") .matchHeader("Authorization", authHeader) .matchHeader("User-Agent", `Hyperwallet Node SDK v${packageJson.version}`) - .matchHeader("Accept", "application/json") + .matchHeader("Accept", "application/jose+json") .matchHeader("Content-Type", "application/jose+json") .put("/", /.+/) - .reply(201, encryptedBody); + .reply(201, encryption.base64Decode(encryptedBody), { "Content-Type": "application/jose+json" }); clientWithEncryption.doPut("test", { message: "Test message" }, {}, (err, body, res) => { err.should.be.deep.equal("Failed to decrypt response for PUT request"); @@ -478,7 +480,7 @@ describe("utils/ApiClient", () => { .filteringPath(() => "/") .matchHeader("Authorization", authHeader) .matchHeader("User-Agent", `Hyperwallet Node SDK v${packageJson.version}`) - .matchHeader("Accept", "application/json") + .matchHeader("Accept", "application/jose+json") .matchHeader("Content-Type", "application/jose+json") .put("/", /.+/) .reply(404, { @@ -486,7 +488,7 @@ describe("utils/ApiClient", () => { "test1", "test2", ], - }); + }, { "Content-Type": "application/jose+json" }); clientWithEncryption.doPut("test", { message: "Test message" }, {}, (err, body, res) => { err.should.be.deep.equal([ @@ -644,9 +646,9 @@ describe("utils/ApiClient", () => { .filteringPath(() => "/") .matchHeader("Authorization", authHeader) .matchHeader("User-Agent", `Hyperwallet Node SDK v${packageJson.version}`) - .matchHeader("Accept", "application/json") + .matchHeader("Accept", "application/jose+json") .get("/") - .reply(200, encryptedBody); + .reply(200, encryption.base64Decode(encryptedBody), { "Content-Type": "application/jose+json" }); clientWithEncryption.doGet("test", {}, (err, body, res) => { expect(err).to.be.undefined(); @@ -687,9 +689,10 @@ describe("utils/ApiClient", () => { const rawRes = { body: "test", status: 200, + type: "application/json", }; - const callback = client.wrapCallback((err, body, res) => { + const callback = client.wrapCallback("POST", (err, body, res) => { expect(err).to.be.undefined(); body.should.be.equal("test"); @@ -711,9 +714,10 @@ describe("utils/ApiClient", () => { ], }, status: 404, + type: "application/json", }; - const callback = client.wrapCallback((err, body, res) => { + const callback = client.wrapCallback("POST", (err, body, res) => { err.should.be.deep.equal(["test1", "test2"]); body.should.be.deep.equal({ errors: [ @@ -734,9 +738,10 @@ describe("utils/ApiClient", () => { const rawRes = { body: "test", status: 404, + type: "application/json", }; - const callback = client.wrapCallback((err, body, res) => { + const callback = client.wrapCallback("POST", (err, body, res) => { err.should.be.deep.equal([{ message: "Could not communicate with test-server", code: "COMMUNICATION_ERROR", @@ -748,5 +753,56 @@ describe("utils/ApiClient", () => { }); callback(new Error(), rawRes); }); + + it("should call callback with 'body' and 'res' and application/jose+json Content-Type", (cb) => { + const clientPath = path.join(__dirname, "..", "resources", "private-jwkset1"); + const hwPath = path.join(__dirname, "..", "resources", "public-jwkset1"); + const clientWithEncryption = new ApiClient("test-username", "test-password", "https://test-server", { + clientPrivateKeySetPath: clientPath, + hyperwalletKeySetPath: hwPath, + }); + + const encryption = new Encryption(clientPath, hwPath); + const testMessage = { + message: "Test message", + }; + + encryption.encrypt(testMessage).then((encryptedBody) => { + const callback = clientWithEncryption.wrapCallback("POST", (err, body, res) => { + expect(err).to.be.undefined(); + expect(res).not.to.be.undefined(); + body.should.be.deep.equal(testMessage); + + cb(); + }); + const rawRes = { + text: JSON.stringify(encryption.base64Decode(encryptedBody)), + status: 200, + type: "application/jose+json", + }; + callback(undefined, rawRes); + }); + }); + + it("should call callback with static error message as 'errors', when Content-Type is wrong", (cb) => { + const client = new ApiClient("test-username", "test-password", "test-server"); + + const rawRes = { + body: "test", + status: 200, + type: "wrongContentType", + }; + + const callback = client.wrapCallback("POST", (err, body, res) => { + err.should.be.deep.equal([{ + message: "Invalid Content-Type specified in Response Header", + }]); + body.should.be.equal("test"); + rawRes.should.be.deep.equal(res); + + cb(); + }); + callback(new Error(), rawRes); + }); }); });