Skip to content

Commit 303d2f9

Browse files
committed
Add encrypted sync and async
1 parent bfe0ebb commit 303d2f9

File tree

2 files changed

+223
-69
lines changed

2 files changed

+223
-69
lines changed

src/services/cloudDevice/cloudDeviceApi.ts

Lines changed: 209 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import Client from "../../client";
2222
import getJsonResponse from "../../helpers/getJsonResponse";
2323
import mergeDeep from "../../utils/mergeDeep";
2424
import { ApplicationInfo } from "../../typings/applicationInfo";
25-
import { ObjectSerializer, CloudDeviceApiRequest, CloudDeviceApiResponse, ConnectedDevicesResponse, DeviceStatusResponse, CloudDeviceApiSecuredRequest, CloudDeviceApiSecuredResponse, SaleToPOISecuredMessage } from "../../typings/cloudDevice/models";
25+
import { ObjectSerializer, CloudDeviceApiRequest, CloudDeviceApiResponse, ConnectedDevicesResponse, DeviceStatusResponse, CloudDeviceApiSecuredRequest, CloudDeviceApiSecuredResponse, SaleToPOISecuredMessage, SaleToPOIRequest } from "../../typings/cloudDevice/models";
2626
import Resource from "../resource";
2727
import { IRequest } from "../../typings/requestOptions";
2828
import NexoSecurityManager from "../../security/nexoSecurityManager";
@@ -44,7 +44,8 @@ class CloudDeviceAPI extends Service {
4444
this.apiKeyRequired = true;
4545
}
4646

47-
private static setApplicationInfo(request: CloudDeviceApiRequest): CloudDeviceApiRequest {
47+
// Add application information to a CloudDevice API request and sets the device POIID.
48+
private static setApplicationInfo(request: CloudDeviceApiRequest, deviceId: string): CloudDeviceApiRequest {
4849
if (request.SaleToPOIRequest.PaymentRequest) {
4950
const applicationInfo = new ApplicationInfo();
5051
const saleToAcquirerData = { applicationInfo };
@@ -56,6 +57,16 @@ class CloudDeviceAPI extends Service {
5657
mergeDeep(request, reqWithAppInfo);
5758
}
5859

60+
if (true) {
61+
request.SaleToPOIRequest = {
62+
...request.SaleToPOIRequest,
63+
MessageHeader: {
64+
...request.SaleToPOIRequest?.MessageHeader,
65+
POIID: deviceId
66+
}
67+
};
68+
}
69+
5970
return ObjectSerializer.serialize(request, "CloudDeviceApiRequest");
6071
}
6172

@@ -67,99 +78,202 @@ class CloudDeviceAPI extends Service {
6778
* @param deviceId - The unique identifier of the payment device that you send this request to (must match POIID in the MessageHeader).
6879
* @returns A promise that resolves to "ok" if the request was successful, or a CloudDeviceApiResponse if there is an error.
6980
*/
70-
public async(merchantAccount: string, deviceId: string, cloudDeviceApiRequest: CloudDeviceApiRequest): Promise<string | CloudDeviceApiResponse> {
81+
public sendAsync(merchantAccount: string, deviceId: string, cloudDeviceApiRequest: CloudDeviceApiRequest): Promise<string | CloudDeviceApiResponse> {
7182
const endpoint = this.baseUrl + "/merchants/{merchantAccount}/devices/{deviceId}/async"
7283
.replace("{" + "merchantAccount" + "}", encodeURIComponent(String(merchantAccount)))
7384
.replace("{" + "deviceId" + "}", encodeURIComponent(String(deviceId)));
7485

7586
const resource = new Resource(this, endpoint);
7687

77-
const request = CloudDeviceAPI.setApplicationInfo(cloudDeviceApiRequest);
78-
79-
// set deviceId
80-
request.SaleToPOIRequest.MessageHeader.POIID = deviceId;
88+
const request = CloudDeviceAPI.setApplicationInfo(cloudDeviceApiRequest, deviceId);
8189

8290
return getJsonResponse<CloudDeviceApiRequest>(
83-
resource,
91+
resource,
8492
request
8593
);
8694
}
8795

88-
/**
89-
* Send an asynchronous payment request.
90-
*
91-
* @param merchantAccount - The unique identifier of the merchant account.
92-
* @param deviceId - The unique identifier of the payment device that you send this request to (must match POIID in the MessageHeader).
93-
* @param cloudDeviceApiRequest - The request to send.
94-
* @param encryptionCredentialDetails - The details of the encryption credential used for encrypting the request payload (nexoBlob)
95-
* @returns A promise that resolves to "ok" if the request was successful, or a CloudDeviceApiResponse if there is an error.
96-
*/
97-
public asyncEncrypted(merchantAccount: string, deviceId: string, cloudDeviceApiRequest: CloudDeviceApiRequest, encryptionCredentialDetails: EncryptionCredentialDetails): Promise<string | CloudDeviceApiSecuredResponse> {
98-
const endpoint = this.baseUrl + "/merchants/{merchantAccount}/devices/{deviceId}/async"
99-
.replace("{" + "merchantAccount" + "}", encodeURIComponent(String(merchantAccount)))
100-
.replace("{" + "deviceId" + "}", encodeURIComponent(String(deviceId)));
101-
102-
const resource = new Resource(this, endpoint);
103-
104-
const formattedRequest = ObjectSerializer.serialize(cloudDeviceApiRequest, "CloudDeviceApiRequest");
105-
106-
const saleToPoiSecuredMessage: SaleToPOISecuredMessage = NexoSecurityManager.encrypt(
107-
cloudDeviceApiRequest.SaleToPOIRequest?.MessageHeader,
108-
JSON.stringify(formattedRequest),
109-
encryptionCredentialDetails,
110-
);
111-
112-
const securedPaymentRequest: CloudDeviceApiSecuredRequest = ObjectSerializer.serialize({
113-
SaleToPOIRequest: saleToPoiSecuredMessage,
114-
}, "CloudDeviceApiSecuredRequest");
115-
116-
117-
//const request = CloudDeviceAPI.setApplicationInfo(cloudDeviceApiRequest);
118-
// set deviceId
119-
//request.SaleToPOIRequest.MessageHeader.POIID = deviceId;
120-
121-
const jsonResponse = getJsonResponse<CloudDeviceApiSecuredRequest, CloudDeviceApiSecuredResponse>(
122-
resource,
123-
securedPaymentRequest
124-
);
125-
126-
const cloudDeviceApiSecuredResponse: CloudDeviceApiSecuredResponse =
127-
ObjectSerializer.deserialize(jsonResponse, "CloudDeviceApiSecuredResponse");
128-
129-
const response = NexoSecurityManager.decrypt(
130-
cloudDeviceApiSecuredResponse.SaleToPOIResponse,
131-
encryptionCredentialDetails,
132-
);
133-
return ObjectSerializer.deserialize(JSON.parse(response), "CloudDeviceApiSecuredResponse");
96+
/**
97+
* Send an asynchronous encrypted payment request.
98+
*
99+
* @param merchantAccount - The unique identifier of the merchant account.
100+
* @param deviceId - The unique identifier of the payment device that you send this request to (must match POIID in the MessageHeader).
101+
* @param cloudDeviceApiRequest - The request to send.
102+
* @param encryptionCredentialDetails - The details of the encryption credential used for encrypting the request payload (nexoBlob)
103+
* @returns A promise that resolves to "ok" if the request was successful, or a CloudDeviceApiResponse if there is an error.
104+
*
105+
* @throws {CloudDeviceApiError} If an error occurs
106+
* @example
107+
* try {
108+
* const response = await client.sendEncryptedAsync(
109+
* "TestMerchant",
110+
* "P400Plus-123456789",
111+
* cloudDeviceApiRequest,
112+
* encryptionCredentialDetails
113+
* );
114+
* console.log("Decrypted response:", response);
115+
* } catch (err) {
116+
* if (err instanceof CloudDeviceApiError) {
117+
* console.error("CloudDevice API failed:", err.message);
118+
* console.error("Cause:", err.cause);
119+
* }
120+
* }
121+
*/
122+
public async sendEncryptedAsync(merchantAccount: string, deviceId: string, cloudDeviceApiRequest: CloudDeviceApiRequest, encryptionCredentialDetails: EncryptionCredentialDetails): Promise<string | CloudDeviceApiResponse> {
123+
124+
try {
125+
126+
const endpoint = this.baseUrl + "/merchants/{merchantAccount}/devices/{deviceId}/async"
127+
.replace("{" + "merchantAccount" + "}", encodeURIComponent(String(merchantAccount)))
128+
.replace("{" + "deviceId" + "}", encodeURIComponent(String(deviceId)));
129+
130+
const resource = new Resource(this, endpoint);
131+
132+
const request = CloudDeviceAPI.setApplicationInfo(cloudDeviceApiRequest, deviceId);
133+
134+
// extract the payload to encrypt (i.e. PaymentRequest)
135+
const payload = this.extractPayloadObject(request.SaleToPOIRequest);
136+
137+
// encrypt the payload and create SaleToPOISecuredMessage
138+
const saleToPoiSecuredMessage: SaleToPOISecuredMessage = NexoSecurityManager.encrypt(
139+
request.SaleToPOIRequest?.MessageHeader,
140+
JSON.stringify(payload),
141+
encryptionCredentialDetails,
142+
);
143+
144+
const securedPaymentRequest: CloudDeviceApiSecuredRequest = ObjectSerializer.serialize({
145+
SaleToPOIRequest: saleToPoiSecuredMessage,
146+
}, "CloudDeviceApiSecuredRequest");
147+
148+
const jsonResponse = await getJsonResponse<CloudDeviceApiSecuredRequest, CloudDeviceApiSecuredResponse>(
149+
resource,
150+
securedPaymentRequest
151+
);
152+
153+
if (typeof jsonResponse === "string") {
154+
// request was successful
155+
return jsonResponse;
156+
}
157+
158+
const cloudDeviceApiSecuredResponse: CloudDeviceApiSecuredResponse =
159+
ObjectSerializer.deserialize(jsonResponse, "CloudDeviceApiSecuredResponse");
160+
161+
// decrypt SaleToPOISecuredMessage
162+
const decryptedPayload = NexoSecurityManager.decrypt(
163+
cloudDeviceApiSecuredResponse.SaleToPOIResponse,
164+
encryptionCredentialDetails,
165+
);
166+
167+
return ObjectSerializer.deserialize(JSON.parse(decryptedPayload), "CloudDeviceApiResponse");
168+
169+
} catch (err: any) {
170+
// an error has occurred
171+
console.error(err);
172+
throw new CloudDeviceApiError(err?.message || "Unknown error", err);
173+
}
134174
}
135-
175+
136176

137177
/**
138178
* Send a synchronous payment request.
179+
*
139180
* @param cloudDeviceApiRequest - The request to send.
140181
* @param merchantAccount - The unique identifier of the merchant account.
141182
* @param deviceId - The unique identifier of the payment device that you send this request to (must match POIID in the MessageHeader).
142183
* @returns A promise that resolves to a CloudDeviceApiResponse.
143184
*/
144-
public async sync(merchantAccount: string, deviceId: string, cloudDeviceApiRequest: CloudDeviceApiRequest): Promise<CloudDeviceApiResponse> {
185+
public async sendSync(merchantAccount: string, deviceId: string, cloudDeviceApiRequest: CloudDeviceApiRequest): Promise<CloudDeviceApiResponse> {
145186
const endpoint = this.baseUrl + "/merchants/{merchantAccount}/devices/{deviceId}/sync"
146187
.replace("{" + "merchantAccount" + "}", encodeURIComponent(String(merchantAccount)))
147188
.replace("{" + "deviceId" + "}", encodeURIComponent(String(deviceId)));
148189

149190
const resource = new Resource(this, endpoint);
150191

151-
const request = CloudDeviceAPI.setApplicationInfo(cloudDeviceApiRequest);
152-
// set deviceId
153-
request.SaleToPOIRequest.MessageHeader.POIID = deviceId;
192+
const request = CloudDeviceAPI.setApplicationInfo(cloudDeviceApiRequest, deviceId);
154193

155194
const response = await getJsonResponse<CloudDeviceApiRequest, CloudDeviceApiResponse>(
156-
resource,
195+
resource,
157196
request
158197
);
159198

160199
return ObjectSerializer.deserialize(response, "CloudDeviceApiResponse");
161200
}
162201

202+
/**
203+
* Send a synchronous encrypted payment request.
204+
*
205+
* @param merchantAccount - The unique identifier of the merchant account.
206+
* @param deviceId - The unique identifier of the payment device that you send this request to (must match POIID in the MessageHeader).
207+
* @param cloudDeviceApiRequest - The request to send.
208+
* @param encryptionCredentialDetails - The details of the encryption credential used for encrypting the request payload (nexoBlob)
209+
* @returns A promise that resolves to CloudDeviceApiSecuredResponse
210+
*
211+
* @throws {CloudDeviceApiError} If an error occurs
212+
* @example
213+
* try {
214+
* const response = await client.sendEncryptedSync(
215+
* "TestMerchant",
216+
* "P400Plus-123456789",
217+
* cloudDeviceApiRequest,
218+
* encryptionCredentialDetails
219+
* );
220+
* console.log("Decrypted response:", response);
221+
* } catch (err) {
222+
* if (err instanceof CloudDeviceApiError) {
223+
* console.error("CloudDevice API failed:", err.message);
224+
* console.error("Cause:", err.cause);
225+
* }
226+
* }
227+
*/
228+
public async sendEncryptedSync(merchantAccount: string, deviceId: string, cloudDeviceApiRequest: CloudDeviceApiRequest, encryptionCredentialDetails: EncryptionCredentialDetails): Promise<CloudDeviceApiResponse> {
229+
230+
try {
231+
232+
const endpoint = this.baseUrl + "/merchants/{merchantAccount}/devices/{deviceId}/sync"
233+
.replace("{" + "merchantAccount" + "}", encodeURIComponent(String(merchantAccount)))
234+
.replace("{" + "deviceId" + "}", encodeURIComponent(String(deviceId)));
235+
236+
const resource = new Resource(this, endpoint);
237+
238+
const request = CloudDeviceAPI.setApplicationInfo(cloudDeviceApiRequest, deviceId);
239+
240+
// extract the payload to encrypt (i.e. PaymentRequest)
241+
const payload = this.extractPayloadObject(request.SaleToPOIRequest);
242+
243+
// encrypt the payload and create SaleToPOISecuredMessage
244+
const saleToPoiSecuredMessage: SaleToPOISecuredMessage = NexoSecurityManager.encrypt(
245+
request.SaleToPOIRequest?.MessageHeader,
246+
JSON.stringify(payload),
247+
encryptionCredentialDetails,
248+
);
249+
250+
const securedPaymentRequest: CloudDeviceApiSecuredRequest = ObjectSerializer.serialize({
251+
SaleToPOIRequest: saleToPoiSecuredMessage,
252+
}, "CloudDeviceApiSecuredRequest");
253+
254+
const jsonResponse = await getJsonResponse<CloudDeviceApiSecuredRequest, CloudDeviceApiSecuredResponse>(
255+
resource,
256+
securedPaymentRequest
257+
);
258+
259+
const cloudDeviceApiSecuredResponse: CloudDeviceApiSecuredResponse =
260+
ObjectSerializer.deserialize(jsonResponse, "CloudDeviceApiSecuredResponse");
261+
262+
// decrypt SaleToPOISecuredMessage
263+
const decryptedPayload = NexoSecurityManager.decrypt(
264+
cloudDeviceApiSecuredResponse.SaleToPOIResponse,
265+
encryptionCredentialDetails,
266+
);
267+
268+
return ObjectSerializer.deserialize(JSON.parse(decryptedPayload), "CloudDeviceApiResponse");
269+
270+
} catch (err: any) {
271+
// an error has occurred
272+
console.error(err);
273+
throw new CloudDeviceApiError(err?.message || "Unknown error", err);
274+
}
275+
}
276+
163277
/**
164278
* Get a list of connected devices for a merchant account.
165279
*
@@ -176,7 +290,7 @@ class CloudDeviceAPI extends Service {
176290
let requestOptions: IRequest.Options = {};
177291
if (store) {
178292
requestOptions.params = { store };
179-
}
293+
}
180294

181295
const response = await getJsonResponse<string, ConnectedDevicesResponse>(
182296
resource,
@@ -210,6 +324,40 @@ class CloudDeviceAPI extends Service {
210324
return ObjectSerializer.deserialize(response, "DeviceStatusResponse");
211325
}
212326

327+
328+
/**
329+
* Extract the payload request object
330+
*/
331+
extractPayloadObject(saleToPOIRequest: SaleToPOIRequest): { [key: string]: any } | null {
332+
for (const attr of SaleToPOIRequest.attributeTypeMap) {
333+
// ignore MessageHeader or SecurityTrailer
334+
if (attr.name === "MessageHeader" || attr.name === "SecurityTrailer") {
335+
continue; // skip header/trailer
336+
}
337+
338+
const value = (saleToPOIRequest as any)[attr.name];
339+
if (value !== undefined && value !== null) {
340+
return { [attr.name]: value };
341+
}
342+
}
343+
return null;
344+
}
345+
346+
}
347+
348+
349+
/**
350+
* CloudDeviceApiError wraps any failure during the processing of Cloud Device API requests
351+
*/
352+
export class CloudDeviceApiError extends Error {
353+
/**
354+
* @param {string} message - A human-readable error message.
355+
* @param {unknown} [cause] - The original error that triggered this failure.
356+
*/
357+
constructor(message: string, public cause?: unknown) {
358+
super(message);
359+
this.name = "CloudDeviceApiError";
360+
}
213361
}
214362

215363
export default CloudDeviceAPI;

0 commit comments

Comments
 (0)