@edge-markets/connect-node 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,9 +58,38 @@ interface EdgeConnectServerConfig {
58
58
  apiBaseUrl?: string // Custom API URL (dev only)
59
59
  authDomain?: string // Custom Cognito domain (dev only)
60
60
  timeout?: number // Request timeout (default: 30000ms)
61
+
62
+ // Optional: Message Level Encryption (Connect endpoints only)
63
+ mle?: {
64
+ enabled: boolean
65
+ edgePublicKey: string // EDGE public encryption key (PEM)
66
+ edgeKeyId: string // EDGE key ID (kid) used for requests
67
+ partnerPrivateKey: string // Your private key (PEM) to decrypt responses
68
+ partnerKeyId: string // Your key ID (kid) expected in response headers
69
+ strictResponseEncryption?: boolean // default true
70
+ }
61
71
  }
62
72
  ```
63
73
 
74
+ ### Message Level Encryption (MLE)
75
+
76
+ ```typescript
77
+ const edge = new EdgeConnectServer({
78
+ clientId: process.env.EDGE_CLIENT_ID!,
79
+ clientSecret: process.env.EDGE_CLIENT_SECRET!,
80
+ environment: 'staging',
81
+ mle: {
82
+ enabled: true,
83
+ edgePublicKey: process.env.EDGE_MLE_EDGE_PUBLIC_KEY!,
84
+ edgeKeyId: process.env.EDGE_MLE_EDGE_KEY_ID!,
85
+ partnerPrivateKey: process.env.EDGE_MLE_PARTNER_PRIVATE_KEY!,
86
+ partnerKeyId: process.env.EDGE_MLE_PARTNER_KEY_ID!,
87
+ },
88
+ })
89
+ ```
90
+
91
+ When enabled, the SDK sends `X-Edge-MLE: v1`, encrypts request bodies, and decrypts encrypted Connect responses.
92
+
64
93
  ## Token Exchange
65
94
 
66
95
  After EdgeLink completes, exchange the code for tokens:
@@ -317,4 +346,3 @@ MIT
317
346
 
318
347
 
319
348
 
320
-
package/dist/index.d.mts CHANGED
@@ -32,6 +32,14 @@ interface EdgeConnectServerConfig {
32
32
  retry?: RetryConfig;
33
33
  onRequest?: (info: RequestInfo) => void;
34
34
  onResponse?: (info: ResponseInfo) => void;
35
+ mle?: {
36
+ enabled: boolean;
37
+ edgePublicKey: string;
38
+ edgeKeyId: string;
39
+ partnerPrivateKey: string;
40
+ partnerKeyId: string;
41
+ strictResponseEncryption?: boolean;
42
+ };
35
43
  }
36
44
  interface TransferOptions {
37
45
  type: 'debit' | 'credit';
package/dist/index.d.ts CHANGED
@@ -32,6 +32,14 @@ interface EdgeConnectServerConfig {
32
32
  retry?: RetryConfig;
33
33
  onRequest?: (info: RequestInfo) => void;
34
34
  onResponse?: (info: ResponseInfo) => void;
35
+ mle?: {
36
+ enabled: boolean;
37
+ edgePublicKey: string;
38
+ edgeKeyId: string;
39
+ partnerPrivateKey: string;
40
+ partnerKeyId: string;
41
+ strictResponseEncryption?: boolean;
42
+ };
35
43
  }
36
44
  interface TransferOptions {
37
45
  type: 'debit' | 'credit';
package/dist/index.js CHANGED
@@ -41,6 +41,83 @@ module.exports = __toCommonJS(index_exports);
41
41
 
42
42
  // src/edge-connect-server.ts
43
43
  var import_connect = require("@edge-markets/connect");
44
+
45
+ // src/mle.ts
46
+ var import_crypto = require("crypto");
47
+ function encryptMle(payload, clientId, config) {
48
+ const header = {
49
+ alg: "RSA-OAEP-256",
50
+ enc: "A256GCM",
51
+ kid: config.edgeKeyId,
52
+ typ: "application/json",
53
+ iat: Math.floor(Date.now() / 1e3),
54
+ jti: (0, import_crypto.randomUUID)(),
55
+ clientId
56
+ };
57
+ const protectedHeaderB64 = toBase64Url(Buffer.from(JSON.stringify(header), "utf8"));
58
+ const aad = Buffer.from(protectedHeaderB64, "utf8");
59
+ const cek = (0, import_crypto.randomBytes)(32);
60
+ const iv = (0, import_crypto.randomBytes)(12);
61
+ const cipher = (0, import_crypto.createCipheriv)("aes-256-gcm", cek, iv);
62
+ cipher.setAAD(aad);
63
+ const plaintext = Buffer.from(JSON.stringify(payload), "utf8");
64
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
65
+ const tag = cipher.getAuthTag();
66
+ const encryptedKey = (0, import_crypto.publicEncrypt)(
67
+ {
68
+ key: config.edgePublicKey,
69
+ padding: import_crypto.constants.RSA_PKCS1_OAEP_PADDING,
70
+ oaepHash: "sha256"
71
+ },
72
+ cek
73
+ );
74
+ return [
75
+ protectedHeaderB64,
76
+ toBase64Url(encryptedKey),
77
+ toBase64Url(iv),
78
+ toBase64Url(ciphertext),
79
+ toBase64Url(tag)
80
+ ].join(".");
81
+ }
82
+ function decryptMle(jwe, config) {
83
+ const parts = jwe.split(".");
84
+ if (parts.length !== 5) {
85
+ throw new Error("Invalid MLE payload format");
86
+ }
87
+ const [protectedHeaderB64, encryptedKeyB64, ivB64, ciphertextB64, tagB64] = parts;
88
+ const protectedHeader = JSON.parse(fromBase64Url(protectedHeaderB64).toString("utf8"));
89
+ if (protectedHeader.alg !== "RSA-OAEP-256" || protectedHeader.enc !== "A256GCM") {
90
+ throw new Error("Unsupported MLE algorithms");
91
+ }
92
+ if (protectedHeader.kid && protectedHeader.kid !== config.partnerKeyId) {
93
+ throw new Error(`Unexpected response key id: ${protectedHeader.kid}`);
94
+ }
95
+ const cek = (0, import_crypto.privateDecrypt)(
96
+ {
97
+ key: config.partnerPrivateKey,
98
+ padding: import_crypto.constants.RSA_PKCS1_OAEP_PADDING,
99
+ oaepHash: "sha256"
100
+ },
101
+ fromBase64Url(encryptedKeyB64)
102
+ );
103
+ const decipher = (0, import_crypto.createDecipheriv)("aes-256-gcm", cek, fromBase64Url(ivB64));
104
+ decipher.setAAD(Buffer.from(protectedHeaderB64, "utf8"));
105
+ decipher.setAuthTag(fromBase64Url(tagB64));
106
+ const plaintext = Buffer.concat([
107
+ decipher.update(fromBase64Url(ciphertextB64)),
108
+ decipher.final()
109
+ ]);
110
+ return JSON.parse(plaintext.toString("utf8"));
111
+ }
112
+ function toBase64Url(buffer) {
113
+ return buffer.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
114
+ }
115
+ function fromBase64Url(value) {
116
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
117
+ return Buffer.from(normalized, "base64");
118
+ }
119
+
120
+ // src/edge-connect-server.ts
44
121
  var DEFAULT_TIMEOUT = 3e4;
45
122
  var USER_AGENT = "@edge-markets/connect-node/1.0.0";
46
123
  var DEFAULT_RETRY_CONFIG = {
@@ -227,6 +304,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
227
304
  async apiRequest(method, path, accessToken, body) {
228
305
  const url = `${this.apiBaseUrl}${path}`;
229
306
  let lastError = null;
307
+ const mleConfig = this.config.mle;
308
+ const mleEnabled = !!mleConfig?.enabled;
230
309
  for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
231
310
  const startTime = Date.now();
232
311
  if (attempt > 0) {
@@ -234,16 +313,42 @@ var EdgeConnectServer = class _EdgeConnectServer {
234
313
  }
235
314
  this.config.onRequest?.({ method, url, body });
236
315
  try {
316
+ const requestHeaders = {
317
+ Authorization: `Bearer ${accessToken}`,
318
+ "Content-Type": "application/json",
319
+ "User-Agent": USER_AGENT
320
+ };
321
+ let requestBody = body;
322
+ if (mleEnabled) {
323
+ requestHeaders["X-Edge-MLE"] = "v1";
324
+ if (body !== void 0) {
325
+ requestBody = { jwe: encryptMle(body, this.config.clientId, mleConfig) };
326
+ }
327
+ }
237
328
  const response = await this.fetchWithTimeout(url, {
238
329
  method,
239
- headers: {
240
- Authorization: `Bearer ${accessToken}`,
241
- "Content-Type": "application/json",
242
- "User-Agent": USER_AGENT
243
- },
244
- body: body ? JSON.stringify(body) : void 0
330
+ headers: requestHeaders,
331
+ body: requestBody !== void 0 ? JSON.stringify(requestBody) : void 0
245
332
  });
246
- const responseBody = await response.json().catch(() => ({}));
333
+ const rawResponseBody = await response.json().catch(() => ({}));
334
+ let responseBody = rawResponseBody;
335
+ if (mleEnabled && typeof rawResponseBody?.jwe === "string") {
336
+ responseBody = decryptMle(rawResponseBody.jwe, mleConfig);
337
+ } else if (!mleEnabled && typeof rawResponseBody?.jwe === "string") {
338
+ throw new import_connect.EdgeApiError(
339
+ "mle_required",
340
+ "The API responded with message-level encryption. Enable MLE in SDK config.",
341
+ response.status,
342
+ rawResponseBody
343
+ );
344
+ } else if (mleEnabled && mleConfig?.strictResponseEncryption !== false && response.ok && typeof rawResponseBody?.jwe !== "string") {
345
+ throw new import_connect.EdgeApiError(
346
+ "mle_response_missing",
347
+ "Expected encrypted response payload but received plaintext.",
348
+ response.status,
349
+ rawResponseBody
350
+ );
351
+ }
247
352
  const durationMs = Date.now() - startTime;
248
353
  this.config.onResponse?.({ status: response.status, body: responseBody, durationMs });
249
354
  if (!response.ok) {
package/dist/index.mjs CHANGED
@@ -11,6 +11,83 @@ import {
11
11
  EdgeNetworkError,
12
12
  EdgeValidationError
13
13
  } from "@edge-markets/connect";
14
+
15
+ // src/mle.ts
16
+ import { randomUUID, randomBytes, createCipheriv, createDecipheriv, publicEncrypt, privateDecrypt, constants } from "crypto";
17
+ function encryptMle(payload, clientId, config) {
18
+ const header = {
19
+ alg: "RSA-OAEP-256",
20
+ enc: "A256GCM",
21
+ kid: config.edgeKeyId,
22
+ typ: "application/json",
23
+ iat: Math.floor(Date.now() / 1e3),
24
+ jti: randomUUID(),
25
+ clientId
26
+ };
27
+ const protectedHeaderB64 = toBase64Url(Buffer.from(JSON.stringify(header), "utf8"));
28
+ const aad = Buffer.from(protectedHeaderB64, "utf8");
29
+ const cek = randomBytes(32);
30
+ const iv = randomBytes(12);
31
+ const cipher = createCipheriv("aes-256-gcm", cek, iv);
32
+ cipher.setAAD(aad);
33
+ const plaintext = Buffer.from(JSON.stringify(payload), "utf8");
34
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
35
+ const tag = cipher.getAuthTag();
36
+ const encryptedKey = publicEncrypt(
37
+ {
38
+ key: config.edgePublicKey,
39
+ padding: constants.RSA_PKCS1_OAEP_PADDING,
40
+ oaepHash: "sha256"
41
+ },
42
+ cek
43
+ );
44
+ return [
45
+ protectedHeaderB64,
46
+ toBase64Url(encryptedKey),
47
+ toBase64Url(iv),
48
+ toBase64Url(ciphertext),
49
+ toBase64Url(tag)
50
+ ].join(".");
51
+ }
52
+ function decryptMle(jwe, config) {
53
+ const parts = jwe.split(".");
54
+ if (parts.length !== 5) {
55
+ throw new Error("Invalid MLE payload format");
56
+ }
57
+ const [protectedHeaderB64, encryptedKeyB64, ivB64, ciphertextB64, tagB64] = parts;
58
+ const protectedHeader = JSON.parse(fromBase64Url(protectedHeaderB64).toString("utf8"));
59
+ if (protectedHeader.alg !== "RSA-OAEP-256" || protectedHeader.enc !== "A256GCM") {
60
+ throw new Error("Unsupported MLE algorithms");
61
+ }
62
+ if (protectedHeader.kid && protectedHeader.kid !== config.partnerKeyId) {
63
+ throw new Error(`Unexpected response key id: ${protectedHeader.kid}`);
64
+ }
65
+ const cek = privateDecrypt(
66
+ {
67
+ key: config.partnerPrivateKey,
68
+ padding: constants.RSA_PKCS1_OAEP_PADDING,
69
+ oaepHash: "sha256"
70
+ },
71
+ fromBase64Url(encryptedKeyB64)
72
+ );
73
+ const decipher = createDecipheriv("aes-256-gcm", cek, fromBase64Url(ivB64));
74
+ decipher.setAAD(Buffer.from(protectedHeaderB64, "utf8"));
75
+ decipher.setAuthTag(fromBase64Url(tagB64));
76
+ const plaintext = Buffer.concat([
77
+ decipher.update(fromBase64Url(ciphertextB64)),
78
+ decipher.final()
79
+ ]);
80
+ return JSON.parse(plaintext.toString("utf8"));
81
+ }
82
+ function toBase64Url(buffer) {
83
+ return buffer.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
84
+ }
85
+ function fromBase64Url(value) {
86
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
87
+ return Buffer.from(normalized, "base64");
88
+ }
89
+
90
+ // src/edge-connect-server.ts
14
91
  var DEFAULT_TIMEOUT = 3e4;
15
92
  var USER_AGENT = "@edge-markets/connect-node/1.0.0";
16
93
  var DEFAULT_RETRY_CONFIG = {
@@ -197,6 +274,8 @@ var EdgeConnectServer = class _EdgeConnectServer {
197
274
  async apiRequest(method, path, accessToken, body) {
198
275
  const url = `${this.apiBaseUrl}${path}`;
199
276
  let lastError = null;
277
+ const mleConfig = this.config.mle;
278
+ const mleEnabled = !!mleConfig?.enabled;
200
279
  for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) {
201
280
  const startTime = Date.now();
202
281
  if (attempt > 0) {
@@ -204,16 +283,42 @@ var EdgeConnectServer = class _EdgeConnectServer {
204
283
  }
205
284
  this.config.onRequest?.({ method, url, body });
206
285
  try {
286
+ const requestHeaders = {
287
+ Authorization: `Bearer ${accessToken}`,
288
+ "Content-Type": "application/json",
289
+ "User-Agent": USER_AGENT
290
+ };
291
+ let requestBody = body;
292
+ if (mleEnabled) {
293
+ requestHeaders["X-Edge-MLE"] = "v1";
294
+ if (body !== void 0) {
295
+ requestBody = { jwe: encryptMle(body, this.config.clientId, mleConfig) };
296
+ }
297
+ }
207
298
  const response = await this.fetchWithTimeout(url, {
208
299
  method,
209
- headers: {
210
- Authorization: `Bearer ${accessToken}`,
211
- "Content-Type": "application/json",
212
- "User-Agent": USER_AGENT
213
- },
214
- body: body ? JSON.stringify(body) : void 0
300
+ headers: requestHeaders,
301
+ body: requestBody !== void 0 ? JSON.stringify(requestBody) : void 0
215
302
  });
216
- const responseBody = await response.json().catch(() => ({}));
303
+ const rawResponseBody = await response.json().catch(() => ({}));
304
+ let responseBody = rawResponseBody;
305
+ if (mleEnabled && typeof rawResponseBody?.jwe === "string") {
306
+ responseBody = decryptMle(rawResponseBody.jwe, mleConfig);
307
+ } else if (!mleEnabled && typeof rawResponseBody?.jwe === "string") {
308
+ throw new EdgeApiError(
309
+ "mle_required",
310
+ "The API responded with message-level encryption. Enable MLE in SDK config.",
311
+ response.status,
312
+ rawResponseBody
313
+ );
314
+ } else if (mleEnabled && mleConfig?.strictResponseEncryption !== false && response.ok && typeof rawResponseBody?.jwe !== "string") {
315
+ throw new EdgeApiError(
316
+ "mle_response_missing",
317
+ "Expected encrypted response payload but received plaintext.",
318
+ response.status,
319
+ rawResponseBody
320
+ );
321
+ }
217
322
  const durationMs = Date.now() - startTime;
218
323
  this.config.onResponse?.({ status: response.status, body: responseBody, durationMs });
219
324
  if (!response.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edge-markets/connect-node",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Server SDK for EDGE Connect token exchange and API calls",
5
5
  "author": "Edge Markets",
6
6
  "license": "MIT",