@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 +29 -1
- package/dist/index.d.mts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +112 -7
- package/dist/index.mjs +112 -7
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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) {
|